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 31972380ff..b4d337da19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,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]) @@ -30,17 +37,24 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - clients,validator-api: take coconut signers from the chain instead of specifying them via CLI ([#1747]) - multisig contract: add DKG contract to the list of addresses that can create proposals ([#1747]) - socks5-client: wait closing inbound connection until data is sent, and throttle incoming data in general ([#1783]) +- nym-cli: improve error reporting/handling and changed `vesting-schedule` queries to use query client instead of signing client ### Fixed - 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 29e2aaf99d..5113e59e0e 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 = [ "async-trait", "client-connections", @@ -612,6 +612,7 @@ dependencies = [ "thiserror", "time 0.3.14", "tokio", + "tokio-stream", "topology", "url", "validator-client", @@ -1649,7 +1650,7 @@ dependencies = [ [[package]] name = "explorer-api" -version = "1.1.0" +version = "1.1.1" dependencies = [ "chrono", "clap 3.2.8", @@ -3139,7 +3140,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "base64", @@ -3156,6 +3157,7 @@ dependencies = [ "pretty_env_logger", "serde", "serde_json", + "tap", "tokio", "validator-client", ] @@ -3181,6 +3183,7 @@ dependencies = [ "rand 0.6.5", "serde", "serde_json", + "tap", "thiserror", "time 0.3.14", "toml", @@ -3191,7 +3194,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.0" +version = "1.1.1" dependencies = [ "clap 3.2.8", "client-connections", @@ -3231,7 +3234,7 @@ dependencies = [ [[package]] name = "nym-gateway" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "async-trait", @@ -3278,7 +3281,7 @@ dependencies = [ [[package]] name = "nym-mixnode" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "bs58", @@ -3320,7 +3323,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.0" +version = "1.1.1" dependencies = [ "async-trait", "clap 3.2.8", @@ -3368,7 +3371,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.0" +version = "1.1.1" dependencies = [ "clap 3.2.8", "client-connections", @@ -3435,7 +3438,7 @@ dependencies = [ [[package]] name = "nym-validator-api" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "async-trait", @@ -5714,8 +5717,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" name = "task" version = "0.1.0" dependencies = [ + "futures", "log", + "thiserror", "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", ] [[package]] diff --git a/clients/client-core/Cargo.toml b/clients/client-core/Cargo.toml index c429e6c58b..7ff4b53078 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" @@ -34,7 +34,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" } +[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" @@ -50,9 +54,6 @@ rev = "b9d1a54ad514c2f230a026afe0dde341e98cd7b6" version = "0.2.4" features = ["futures"] -[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..f2c9668166 --- /dev/null +++ b/clients/client-core/src/client/base_client/mod.rs @@ -0,0 +1,481 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::cover_traffic_stream::LoopCoverTrafficStream; +use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}; +use crate::client::key_manager::KeyManager; +use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController}; +use crate::client::real_messages_control; +use crate::client::real_messages_control::RealMessagesController; +use crate::client::received_buffer::{ + ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, +}; +use crate::client::replies::reply_controller; +use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyControllerSender}; +use crate::client::replies::reply_storage::{ + CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys, +}; +use crate::client::topology_control::{ + TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, +}; +use crate::config::{Config, DebugConfig, GatewayEndpointConfig}; +use crate::error::ClientCoreError; +use crate::spawn_future; +use client_connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; +use crypto::asymmetric::{encryption, identity}; +use futures::channel::mpsc; +use gateway_client::bandwidth::BandwidthController; +use gateway_client::{ + AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, + MixnetMessageSender, +}; +use log::info; +use nymsphinx::acknowledgements::AckKey; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::addressing::nodes::NodeIdentity; +use std::sync::Arc; +use std::time::Duration; +use task::{ShutdownListener, ShutdownNotifier}; +use url::Url; + +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +pub mod non_wasm_helpers; + +pub struct ClientInput { + pub shared_lane_queue_lengths: LaneQueueLengths, + pub connection_command_sender: ConnectionCommandSender, + pub input_sender: InputMessageSender, +} + +pub struct ClientOutput { + pub received_buffer_request_sender: ReceivedBufferRequestSender, +} + +pub enum ClientInputStatus { + AwaitingProducer { client_input: ClientInput }, + Connected, +} + +impl ClientInputStatus { + pub fn register_producer(&mut self) -> ClientInput { + match std::mem::replace(self, ClientInputStatus::Connected) { + ClientInputStatus::AwaitingProducer { client_input } => client_input, + ClientInputStatus::Connected => panic!("producer was already registered before"), + } + } +} + +pub enum ClientOutputStatus { + AwaitingConsumer { client_output: ClientOutput }, + Connected, +} + +impl ClientOutputStatus { + pub fn register_consumer(&mut self) -> ClientOutput { + match std::mem::replace(self, ClientOutputStatus::Connected) { + ClientOutputStatus::AwaitingConsumer { client_output } => client_output, + ClientOutputStatus::Connected => panic!("consumer was already registered before"), + } + } +} + +pub struct BaseClientBuilder<'a, B> { + // due to wasm limitations I had to split it like this : ( + gateway_config: &'a GatewayEndpointConfig, + debug_config: &'a DebugConfig, + disabled_credentials: bool, + validator_api_endpoints: Vec, + reply_storage_backend: B, + + bandwidth_controller: Option, + key_manager: KeyManager, +} + +impl<'a, B> BaseClientBuilder<'a, B> +where + B: ReplyStorageBackend + Send + Sync + 'static, +{ + pub fn new_from_base_config( + base_config: &'a Config, + key_manager: KeyManager, + bandwidth_controller: Option, + reply_storage_backend: B, + ) -> BaseClientBuilder<'a, B> { + BaseClientBuilder { + gateway_config: base_config.get_gateway_endpoint_config(), + debug_config: base_config.get_debug_config(), + disabled_credentials: base_config.get_disabled_credentials_mode(), + validator_api_endpoints: base_config.get_validator_api_endpoints(), + bandwidth_controller, + reply_storage_backend, + key_manager, + } + } + + pub fn new( + gateway_config: &'a GatewayEndpointConfig, + debug_config: &'a DebugConfig, + key_manager: KeyManager, + bandwidth_controller: Option, + reply_storage_backend: B, + disabled_credentials: bool, + validator_api_endpoints: Vec, + ) -> BaseClientBuilder<'a, B> { + BaseClientBuilder { + gateway_config, + debug_config, + disabled_credentials, + validator_api_endpoints, + bandwidth_controller, + reply_storage_backend, + key_manager, + } + } + + pub fn as_mix_recipient(&self) -> Recipient { + Recipient::new( + *self.key_manager.identity_keypair().public_key(), + *self.key_manager.encryption_keypair().public_key(), + // TODO: below only works under assumption that gateway address == gateway id + // (which currently is true) + NodeIdentity::from_base58_string(&self.gateway_config.gateway_id).unwrap(), + ) + } + + // future constantly pumping loop cover traffic at some specified average rate + // the pumped traffic goes to the MixTrafficController + fn start_cover_traffic_stream( + debug_config: &DebugConfig, + ack_key: Arc, + self_address: Recipient, + topology_accessor: TopologyAccessor, + mix_tx: BatchMixMessageSender, + shutdown: ShutdownListener, + ) { + info!("Starting loop cover traffic stream..."); + + let mut stream = LoopCoverTrafficStream::new( + ack_key, + debug_config.average_ack_delay, + debug_config.average_packet_delay, + debug_config.loop_cover_traffic_average_delay, + mix_tx, + self_address, + topology_accessor, + ); + + if let Some(size) = debug_config.use_extended_packet_size { + log::debug!("Setting extended packet size: {:?}", size); + stream.set_custom_packet_size(size.into()); + } + + stream.start_with_shutdown(shutdown); + } + + #[allow(clippy::too_many_arguments)] + fn start_real_traffic_controller( + controller_config: real_messages_control::Config, + topology_accessor: TopologyAccessor, + ack_receiver: AcknowledgementReceiver, + input_receiver: InputMessageReceiver, + mix_sender: BatchMixMessageSender, + reply_storage: CombinedReplyStorage, + reply_controller_sender: ReplyControllerSender, + reply_controller_receiver: ReplyControllerReceiver, + lane_queue_lengths: LaneQueueLengths, + client_connection_rx: ConnectionCommandReceiver, + shutdown: ShutdownListener, + ) { + info!("Starting real traffic stream..."); + + RealMessagesController::new( + controller_config, + ack_receiver, + input_receiver, + mix_sender, + topology_accessor, + reply_storage, + reply_controller_sender, + reply_controller_receiver, + lane_queue_lengths, + client_connection_rx, + ) + .start_with_shutdown(shutdown); + } + + // buffer controlling all messages fetched from provider + // required so that other components would be able to use them (say the websocket) + fn start_received_messages_buffer_controller( + local_encryption_keypair: Arc, + query_receiver: ReceivedBufferRequestReceiver, + mixnet_receiver: MixnetMessageReceiver, + reply_key_storage: SentReplyKeys, + reply_controller_sender: ReplyControllerSender, + shutdown: ShutdownListener, + ) { + info!("Starting received messages buffer controller..."); + ReceivedMessagesBufferController::new( + local_encryption_keypair, + query_receiver, + mixnet_receiver, + reply_key_storage, + reply_controller_sender, + ) + .start_with_shutdown(shutdown) + } + + async fn start_gateway_client( + &mut self, + mixnet_message_sender: MixnetMessageSender, + ack_sender: AcknowledgementSender, + shutdown: ShutdownListener, + ) -> 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(), + 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( + validator_api_urls: Vec, + refresh_rate: Duration, + topology_accessor: TopologyAccessor, + shutdown: ShutdownListener, + ) -> Result<(), ClientCoreError> { + let topology_refresher_config = TopologyRefresherConfig::new( + validator_api_urls, + refresh_rate, + env!("CARGO_PKG_VERSION").to_string(), + ); + let mut topology_refresher = + TopologyRefresher::new(topology_refresher_config, topology_accessor); + // before returning, block entire runtime to refresh the current network view so that any + // components depending on topology would see a non-empty view + info!("Obtaining initial network topology"); + topology_refresher.refresh().await; + + // 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 + } + + async fn setup_persistent_reply_storage( + backend: B, + shutdown: ShutdownListener, + ) -> Result> { + let persistent_storage = PersistentReplyStorage::new(backend); + let mem_store = persistent_storage + .load_state_from_backend() + .await + .map_err(|err| ClientCoreError::SurbStorageError { source: err })?; + + let store_clone = mem_store.clone(); + spawn_future(async move { + persistent_storage + .flush_on_shutdown(store_clone, shutdown) + .await + }); + + Ok(mem_store) + } + + pub async fn start_base(mut self) -> Result> { + info!("Starting nym client"); + // channels for inter-component communication + // TODO: make the channels be internally created by the relevant components + // rather than creating them here, so say for example the buffer controller would create the request channels + // and would allow anyone to clone the sender channel + + // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway + // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer + let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); + + // used for announcing connection or disconnection of a channel for pushing re-assembled messages to + let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); + + // channels responsible for controlling real messages + let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); + + // channels responsible for controlling ack messages + let (ack_sender, ack_receiver) = mpsc::unbounded(); + let shared_topology_accessor = TopologyAccessor::new(); + + // Shutdown notifier for signalling tasks to stop + let shutdown = ShutdownNotifier::default(); + + // channels responsible for dealing with reply-related fun + let (reply_controller_sender, reply_controller_receiver) = + reply_controller::new_control_channels(); + + let self_address = self.as_mix_recipient(); + + // the components are started in very specific order. Unless you know what you are doing, + // do not change that. + let gateway_client = self + .start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe()) + .await; + + let reply_storage = + Self::setup_persistent_reply_storage(self.reply_storage_backend, shutdown.subscribe()) + .await?; + + Self::start_topology_refresher( + self.validator_api_endpoints.clone(), + self.debug_config.topology_refresh_rate, + shared_topology_accessor.clone(), + shutdown.subscribe(), + ) + .await?; + + Self::start_received_messages_buffer_controller( + self.key_manager.encryption_keypair(), + received_buffer_request_receiver, + mixnet_messages_receiver, + reply_storage.key_storage(), + reply_controller_sender.clone(), + shutdown.subscribe(), + ); + + // The sphinx_message_sender is the transmitter for any component generating sphinx packets + // that are to be sent to the mixnet. They are used by cover traffic stream and real + // traffic stream. + // The MixTrafficController then sends the actual traffic + let sphinx_message_sender = + Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe()); + + // Channels that the websocket listener can use to signal downstream to the real traffic + // controller that connections are closed. + let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); + + // Shared queue length data. Published by the `OutQueueController` in the client, and used + // primarily to throttle incoming connections (e.g socks5 for attached network-requesters) + let shared_lane_queue_lengths = LaneQueueLengths::new(); + + let mut controller_config = real_messages_control::Config::new( + self.debug_config, + self.key_manager.ack_key(), + self_address, + ); + + if let Some(size) = self.debug_config.use_extended_packet_size { + log::debug!("Setting extended packet size: {:?}", size); + controller_config.set_custom_packet_size(size.into()); + } + + Self::start_real_traffic_controller( + controller_config, + shared_topology_accessor.clone(), + ack_receiver, + input_receiver, + sphinx_message_sender.clone(), + reply_storage, + reply_controller_sender, + reply_controller_receiver, + shared_lane_queue_lengths.clone(), + client_connection_rx, + shutdown.subscribe(), + ); + + if !self.debug_config.disable_loop_cover_traffic_stream { + Self::start_cover_traffic_stream( + self.debug_config, + self.key_manager.ack_key(), + self_address, + shared_topology_accessor, + sphinx_message_sender, + shutdown.subscribe(), + ); + } + + info!("Client startup finished!"); + info!("The address of this client is: {self_address}"); + + Ok(BaseClient { + client_input: ClientInputStatus::AwaitingProducer { + client_input: ClientInput { + shared_lane_queue_lengths, + connection_command_sender: client_connection_tx, + input_sender, + }, + }, + client_output: ClientOutputStatus::AwaitingConsumer { + client_output: ClientOutput { + received_buffer_request_sender, + }, + }, + shutdown_notifier: shutdown, + }) + } +} + +pub struct BaseClient { + pub client_input: ClientInputStatus, + pub client_output: ClientOutputStatus, + + pub shutdown_notifier: ShutdownNotifier, +} diff --git a/clients/client-core/src/client/base_client/non_wasm_helpers.rs b/clients/client-core/src/client/base_client/non_wasm_helpers.rs new file mode 100644 index 0000000000..8c51c784c8 --- /dev/null +++ b/clients/client-core/src/client/base_client/non_wasm_helpers.rs @@ -0,0 +1,51 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::{ + fs_backend, CombinedReplyStorage, ReplyStorageBackend, +}; +use crate::config::DebugConfig; +use crate::error::ClientCoreError; +use log::{error, info}; +use std::path::Path; + +pub async fn setup_fs_reply_surb_backend>( + db_path: P, + debug_config: &DebugConfig, +) -> Result> { + // if the database file doesnt exist, initialise fresh storage, otherwise attempt to load the existing one + let db_path = db_path.as_ref(); + if db_path.exists() { + info!("loading existing surb database"); + match fs_backend::Backend::try_load(db_path).await { + Ok(backend) => Ok(backend), + Err(err) => { + error!("failed to setup persistent storage backend for our reply needs: {err}"); + Err(ClientCoreError::SurbStorageError { source: err }) + } + } + } else { + info!("creating fresh surb database"); + let mut storage_backend = match fs_backend::Backend::init(db_path).await { + Ok(backend) => backend, + Err(err) => { + error!("failed to setup persistent storage backend for our reply needs: {err}"); + return Err(ClientCoreError::SurbStorageError { source: err }); + } + }; + + // while I kinda hate that we're going to be creating `CombinedReplyStorage` twice, + // it will only be happening on the very first run and in practice won't incur huge + // costs since the storage is going to be empty + let mem_store = CombinedReplyStorage::new( + debug_config.minimum_reply_surb_storage_threshold, + debug_config.maximum_reply_surb_storage_threshold, + ); + storage_backend + .init_fresh(&mem_store) + .await + .map_err(|err| ClientCoreError::SurbStorageError { source: err })?; + + Ok(storage_backend) + } +} diff --git a/clients/client-core/src/client/cover_traffic_stream.rs b/clients/client-core/src/client/cover_traffic_stream.rs index f0f5079f2b..56dd0f3751 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!"); @@ -203,12 +213,11 @@ impl LoopCoverTrafficStream { tokio::task::yield_now().await; } - #[cfg(not(target_arch = "wasm32"))] pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) { // we should set initial delay only when we actually start the stream let sampled = sample_poisson_duration(&mut self.rng, self.average_cover_message_sending_delay); - self.next_delay = Box::pin(time::sleep(sampled)); + self.set_next_delay(sampled); spawn_future(async move { debug!("Started LoopCoverTrafficStream with graceful shutdown support"); @@ -229,17 +238,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 d209d0ca4a..4e12121e4d 100644 --- a/clients/client-core/src/client/mix_traffic.rs +++ b/clients/client-core/src/client/mix_traffic.rs @@ -77,7 +77,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"); @@ -98,12 +97,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 c1e382fab0..8110d52909 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; @@ -8,10 +10,3 @@ pub mod real_messages_control; pub mod received_buffer; pub mod replies; 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 c05a3c4ed8..59aacf253f 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 @@ -71,7 +71,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"); @@ -89,11 +88,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 16f5020f5d..d4f86f0e83 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 @@ -242,7 +242,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"); @@ -269,11 +268,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 0f667024e6..9ec3bce229 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 @@ -109,7 +109,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"); @@ -129,11 +128,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 eb5f9aa494..7989af1200 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 @@ -252,7 +252,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; @@ -298,7 +297,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 14f9205700..6130085598 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 @@ -230,7 +230,6 @@ where .await } - #[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"); @@ -248,11 +247,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 43d43bb7a6..2d8e9d6160 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 @@ -43,7 +43,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"); @@ -67,7 +66,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 76fc5f9e43..a189016f53 100644 --- a/clients/client-core/src/client/real_messages_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/mod.rs @@ -35,8 +35,6 @@ use std::time::Duration; use crate::config; pub(crate) use acknowledgement_control::{AckActionSender, Action}; -// #[cfg(feature = "reply-surb")] -// use crate::client::reply_key_storage::ReplyKeyStorage; pub(crate) mod acknowledgement_control; pub(crate) mod message_handler; @@ -91,7 +89,7 @@ pub struct Config { impl Config { pub fn new( - base_client_debug_config: &config::Debug, + base_client_debug_config: &config::DebugConfig, ack_key: Arc, self_recipient: Recipient, ) -> Self { @@ -239,7 +237,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 a69483bec2..a1eb8d2d8c 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 @@ -493,49 +493,73 @@ 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: {}", self.sending_delay_controller.current_multiplier() - ) - }; - if packets > 1000 { - log::warn!("{status_str}"); - } else if packets > 0 { - log::info!("{status_str}"); - } else { - log::debug!("{status_str}"); + ); } } - #[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 3ba8977380..3a3c760888 100644 --- a/clients/client-core/src/client/received_buffer.rs +++ b/clients/client-core/src/client/received_buffer.rs @@ -405,7 +405,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() { @@ -425,11 +424,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"); @@ -455,7 +455,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() { @@ -474,11 +473,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"); @@ -516,7 +516,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/replies/reply_controller.rs b/clients/client-core/src/client/replies/reply_controller.rs index 6ae16cd5be..cd2e3c1d96 100644 --- a/clients/client-core/src/client/replies/reply_controller.rs +++ b/clients/client-core/src/client/replies/reply_controller.rs @@ -478,12 +478,18 @@ where } } - #[cfg(not(target_arch = "wasm32"))] pub(crate) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { debug!("Started ReplyController with graceful shutdown support"); let polling_rate = Duration::from_secs(5); - let mut interval_timer = tokio::time::interval(polling_rate); + + #[cfg(not(target_arch = "wasm32"))] + let mut interval = + tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(polling_rate)); + + #[cfg(target_arch = "wasm32")] + let mut interval = + gloo_timers::future::IntervalStream::new(polling_rate.as_millis() as u32); while !shutdown.is_shutdown() { tokio::select! { @@ -498,7 +504,7 @@ where break; } }, - _ = interval_timer.tick() => { + _ = interval.next() => { self.inspect_stale_entries().await }, } diff --git a/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs b/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs index 422d9d7b2b..9711ea759f 100644 --- a/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs +++ b/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs @@ -11,6 +11,17 @@ pub struct Backend { empty: Empty, } +impl Backend { + pub fn new(min_surb_threshold: usize, max_surb_threshold: usize) -> Self { + Backend { + empty: Empty { + min_surb_threshold, + max_surb_threshold, + }, + } + } +} + #[async_trait] impl ReplyStorageBackend for Backend { type StorageError = ::StorageError; diff --git a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs index d7ece9fca6..cab3e519a7 100644 --- a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs +++ b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs @@ -9,7 +9,7 @@ use log::{error, info}; use sqlx::ConnectOptions; use std::path::Path; -#[derive(Clone)] +#[derive(Debug, Clone)] pub(crate) struct StorageManager { pub(crate) connection_pool: sqlx::SqlitePool, } diff --git a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs index 036e34f1c7..be542e131d 100644 --- a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs +++ b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs @@ -22,6 +22,7 @@ mod error; mod manager; mod models; +#[derive(Debug)] pub struct Backend { temporary_old_path: Option, database_path: PathBuf, diff --git a/clients/client-core/src/client/replies/reply_storage/backend/mod.rs b/clients/client-core/src/client/replies/reply_storage/backend/mod.rs index dcdf88393e..d80231ae1e 100644 --- a/clients/client-core/src/client/replies/reply_storage/backend/mod.rs +++ b/clients/client-core/src/client/replies/reply_storage/backend/mod.rs @@ -53,7 +53,7 @@ impl ReplyStorageBackend for Empty { #[async_trait] pub trait ReplyStorageBackend: Sized { - type StorageError: Error; + type StorageError: Error + 'static; async fn start_storage_session(&self) -> Result<(), Self::StorageError> { Ok(()) diff --git a/clients/client-core/src/client/replies/reply_storage/key_storage.rs b/clients/client-core/src/client/replies/reply_storage/key_storage.rs index e9f45d2abd..05c6466bc5 100644 --- a/clients/client-core/src/client/replies/reply_storage/key_storage.rs +++ b/clients/client-core/src/client/replies/reply_storage/key_storage.rs @@ -6,7 +6,7 @@ use nymsphinx::anonymous_replies::encryption_key::EncryptionKeyDigest; use nymsphinx::anonymous_replies::SurbEncryptionKey; use std::sync::Arc; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] use dashmap::iter::Iter; #[derive(Debug, Clone)] @@ -30,7 +30,7 @@ impl SentReplyKeys { } } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] pub(crate) fn from_raw(raw: Vec<(EncryptionKeyDigest, SurbEncryptionKey)>) -> SentReplyKeys { SentReplyKeys { inner: Arc::new(SentReplyKeysInner { @@ -39,7 +39,7 @@ impl SentReplyKeys { } } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] pub(crate) fn as_raw_iter(&self) -> Iter<'_, EncryptionKeyDigest, SurbEncryptionKey> { self.inner.data.iter() } diff --git a/clients/client-core/src/client/replies/reply_storage/mod.rs b/clients/client-core/src/client/replies/reply_storage/mod.rs index e7692ccc64..83c0e0e34f 100644 --- a/clients/client-core/src/client/replies/reply_storage/mod.rs +++ b/clients/client-core/src/client/replies/reply_storage/mod.rs @@ -34,7 +34,6 @@ where } // this will have to get enabled after merging develop - #[cfg(not(target_arch = "wasm32"))] pub async fn flush_on_shutdown( mut self, mem_state: CombinedReplyStorage, diff --git a/clients/client-core/src/client/replies/reply_storage/surb_storage.rs b/clients/client-core/src/client/replies/reply_storage/surb_storage.rs index b4fc1ab379..e511d3d450 100644 --- a/clients/client-core/src/client/replies/reply_storage/surb_storage.rs +++ b/clients/client-core/src/client/replies/reply_storage/surb_storage.rs @@ -12,7 +12,7 @@ use std::sync::Arc; #[cfg(not(target_arch = "wasm32"))] use tokio::time::Instant; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] use dashmap::iter::Iter; #[cfg(target_arch = "wasm32")] @@ -48,7 +48,7 @@ impl ReceivedReplySurbsMap { } } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] pub(crate) fn from_raw( min_surb_threshold: usize, max_surb_threshold: usize, @@ -63,7 +63,7 @@ impl ReceivedReplySurbsMap { } } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] pub(crate) fn as_raw_iter(&self) -> Iter<'_, AnonymousSenderTag, ReceivedReplySurbs> { self.inner.data.iter() } @@ -211,7 +211,7 @@ impl ReceivedReplySurbs { } } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] pub(crate) fn new_retrieved( surbs: Vec, surbs_last_received_at: Instant, @@ -223,7 +223,7 @@ impl ReceivedReplySurbs { } } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] pub(crate) fn surbs_ref(&self) -> &VecDeque { &self.data } diff --git a/clients/client-core/src/client/replies/reply_storage/tag_storage.rs b/clients/client-core/src/client/replies/reply_storage/tag_storage.rs index bfec6cf045..c91d2b003e 100644 --- a/clients/client-core/src/client/replies/reply_storage/tag_storage.rs +++ b/clients/client-core/src/client/replies/reply_storage/tag_storage.rs @@ -6,7 +6,7 @@ use nymsphinx::addressing::clients::{Recipient, RecipientBytes}; use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use std::sync::Arc; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] use dashmap::iter::Iter; #[derive(Debug, Clone)] @@ -28,7 +28,7 @@ impl UsedSenderTags { } } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] pub(crate) fn from_raw(raw: Vec<(RecipientBytes, AnonymousSenderTag)>) -> UsedSenderTags { UsedSenderTags { inner: Arc::new(UsedSenderTagsInner { @@ -37,7 +37,7 @@ impl UsedSenderTags { } } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] pub(crate) fn as_raw_iter(&self) -> Iter<'_, RecipientBytes, AnonymousSenderTag> { self.inner.data.iter() } diff --git a/clients/client-core/src/client/topology_control.rs b/clients/client-core/src/client/topology_control.rs index 0299d58beb..368e534578 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; @@ -9,7 +10,6 @@ use rand::seq::SliceRandom; use rand::thread_rng; use std::ops::Deref; use std::sync::Arc; -use std::time; use std::time::Duration; use thiserror::Error; use tokio::sync::{RwLock, RwLockReadGuard}; @@ -176,14 +176,14 @@ impl Default for TopologyAccessor { pub struct TopologyRefresherConfig { validator_api_urls: Vec, - refresh_rate: time::Duration, + refresh_rate: Duration, client_version: String, } impl TopologyRefresherConfig { pub fn new( validator_api_urls: Vec, - refresh_rate: time::Duration, + refresh_rate: Duration, client_version: String, ) -> Self { TopologyRefresherConfig { @@ -353,14 +353,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() => { @@ -368,19 +376,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 a956e9ca89..23ce0ef54d 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -51,7 +51,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, @@ -59,15 +59,21 @@ 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, + { Config::default().with_id(id) } - pub fn with_id>(mut self, id: S) -> Self { + pub fn with_id>(mut self, id: S) -> Self + where + T: NymConfig, + { let id = id.into(); // identity key setting @@ -128,7 +134,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; } @@ -146,8 +152,15 @@ impl Config { pub fn set_high_default_traffic_volume(&mut self) { self.debug.average_packet_delay = Duration::from_millis(10); - self.debug.loop_cover_traffic_average_delay = Duration::from_millis(2_000_000); // basically don't really send cover messages - self.debug.message_sending_average_delay = Duration::from_millis(4); // 250 "real" messages / s + // basically don't really send cover messages + self.debug.loop_cover_traffic_average_delay = Duration::from_millis(2_000_000); + // 250 "real" messages / s + self.debug.message_sending_average_delay = Duration::from_millis(4); + } + + pub fn set_no_cover_traffic(&mut self) { + self.debug.disable_loop_cover_traffic_stream = true; + self.debug.disable_main_poisson_packet_distribution = true; } pub fn set_custom_version(&mut self, version: &str) { @@ -158,10 +171,6 @@ impl Config { self.client.id.clone() } - pub fn get_debug_config(&self) -> &Debug { - &self.debug - } - pub fn get_disabled_credentials_mode(&self) -> bool { self.client.disabled_credentials_mode } @@ -214,7 +223,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 } @@ -231,6 +244,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 } @@ -320,7 +337,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, @@ -332,10 +349,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, @@ -343,7 +360,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")] @@ -358,6 +375,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. @@ -384,7 +402,7 @@ pub struct Client { ack_key_file: 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, @@ -458,13 +476,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 @@ -562,9 +580,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..b993c291ae 100644 --- a/clients/client-core/src/error.rs +++ b/clients/client-core/src/error.rs @@ -1,12 +1,13 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::client::replies::reply_storage::ReplyStorageBackend; use crypto::asymmetric::identity::Ed25519RecoveryError; use gateway_client::error::GatewayClientError; use validator_client::ValidatorClientError; #[derive(thiserror::Error, Debug)] -pub enum ClientCoreError { +pub enum ClientCoreError { #[error("I/O error: {0}")] IoError(#[from] std::io::Error), #[error("Gateway client error: {0}")] @@ -26,4 +27,10 @@ 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, + + #[error("experienced a failure with our reply surb persistent storage: {source}")] + SurbStorageError { source: B::StorageError }, } diff --git a/clients/client-core/src/init.rs b/clients/client-core/src/init.rs index be9758d363..4a4b0ccecc 100644 --- a/clients/client-core/src/init.rs +++ b/clients/client-core/src/init.rs @@ -18,16 +18,20 @@ use tap::TapFallible; use topology::{filter::VersionFilterable, gateway}; use url::Url; +use crate::client::replies::reply_storage::ReplyStorageBackend; use crate::{ client::key_manager::KeyManager, config::{persistence::key_pathfinder::ClientKeyPathfinder, Config}, error::ClientCoreError, }; -pub async fn query_gateway_details( +pub async fn query_gateway_details( validator_servers: Vec, chosen_gateway_id: Option<&str>, -) -> Result { +) -> Result> +where + B: ReplyStorageBackend, +{ let validator_api = validator_servers .choose(&mut thread_rng()) .ok_or(ClientCoreError::ListOfValidatorApisIsEmpty)?; @@ -59,12 +63,13 @@ pub async fn query_gateway_details( } } -pub async fn register_with_gateway_and_store_keys( +pub async fn register_with_gateway_and_store_keys( gateway_details: gateway::Node, config: &Config, -) -> Result<(), ClientCoreError> +) -> Result<(), ClientCoreError> where T: NymConfig, + B: ReplyStorageBackend, { let mut rng = OsRng; let mut key_manager = KeyManager::new(&mut rng); @@ -79,10 +84,13 @@ where .tap_err(|err| log::error!("Failed to generate keys: {err}"))?) } -async fn register_with_gateway( +async fn register_with_gateway( gateway: &gateway::Node, our_identity: Arc, -) -> Result, ClientCoreError> { +) -> Result, ClientCoreError> +where + B: ReplyStorageBackend, +{ let timeout = Duration::from_millis(1500); let mut gateway_client = GatewayClient::new_init( gateway.clients_address(), @@ -90,8 +98,6 @@ async fn register_with_gateway( gateway.owner.clone(), our_identity.clone(), timeout, - #[cfg(not(target_arch = "wasm32"))] - None, ); gateway_client .establish_connection() @@ -104,13 +110,17 @@ async fn register_with_gateway( Ok(shared_keys) } -pub fn show_address(config: &Config) -> Result<(), ClientCoreError> +pub fn show_address(config: &Config) -> Result<(), ClientCoreError> where T: config::NymConfig, + B: ReplyStorageBackend, { - fn load_identity_keys( + fn load_identity_keys( pathfinder: &ClientKeyPathfinder, - ) -> Result { + ) -> Result> + where + B: ReplyStorageBackend, + { let identity_keypair: identity::KeyPair = pemstore::load_keypair(&pemstore::KeyPairPath::new( pathfinder.private_identity_key().to_owned(), @@ -120,9 +130,12 @@ where Ok(identity_keypair) } - fn load_sphinx_keys( + fn load_sphinx_keys( pathfinder: &ClientKeyPathfinder, - ) -> Result { + ) -> Result> + where + B: ReplyStorageBackend, + { let sphinx_keypair: encryption::KeyPair = pemstore::load_keypair(&pemstore::KeyPairPath::new( pathfinder.private_encryption_key().to_owned(), diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 0415974eb3..585302e5e4 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 f031a0e816..6a1f59cd5c 100644 --- a/clients/native/src/client/config/mod.rs +++ b/clients/native/src/client/config/mod.rs @@ -3,7 +3,7 @@ use crate::client::config::template::config_template; pub use client_core::config::MISSING_VALUE; -use client_core::config::{Config as BaseConfig, Debug}; +use client_core::config::{Config as BaseConfig, DebugConfig}; use config::defaults::DEFAULT_WEBSOCKET_LISTENING_PORT; use config::NymConfig; use serde::{Deserialize, Serialize}; @@ -27,6 +27,10 @@ impl SocketType { _ => SocketType::None, } } + + pub fn is_websocket(&self) -> bool { + matches!(self, SocketType::WebSocket) + } } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] @@ -100,7 +104,7 @@ impl Config { &mut self.base } - pub fn get_debug_settings(&self) -> &Debug { + pub fn get_debug_settings(&self) -> &DebugConfig { self.get_base().get_debug_config() } diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 6673b5fabd..a4e4867c6c 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -1,208 +1,48 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::config::{Config, SocketType}; +use crate::client::config::Config; use crate::error::ClientError; use crate::websocket; -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_connections::TransmissionLane; +use client_core::client::base_client::{ + non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput, }; +use client_core::client::inbound_messages::InputMessage; use client_core::client::key_manager::KeyManager; -use client_core::client::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::replies::reply_controller; -use client_core::client::replies::reply_controller::{ - ReplyControllerReceiver, ReplyControllerSender, -}; -use client_core::client::replies::reply_storage::{ - fs_backend, CombinedReplyStorage, PersistentReplyStorage, SentReplyKeys, -}; -use client_core::client::topology_control::{ - TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, -}; +use client_core::client::received_buffer::{ReceivedBufferMessage, ReconstructedMessagesReceiver}; 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::requests::AnonymousSenderTag; use nymsphinx::receiver::ReconstructedMessage; -use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; +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, - ack_receiver: AcknowledgementReceiver, - input_receiver: InputMessageReceiver, - mix_sender: BatchMixMessageSender, - reply_storage: CombinedReplyStorage, - reply_controller_sender: ReplyControllerSender, - reply_controller_receiver: ReplyControllerReceiver, - lane_queue_lengths: LaneQueueLengths, - client_connection_rx: ConnectionCommandReceiver, - shutdown: ShutdownListener, - ) { - let mut controller_config = real_messages_control::Config::new( - self.config.get_debug_settings(), - self.key_manager.ack_key(), - 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_storage, - reply_controller_sender, - reply_controller_receiver, - lane_queue_lengths, - client_connection_rx, - ) - .start_with_shutdown(shutdown); - } - - // buffer controlling all messages fetched from provider - // required so that other components would be able to use them (say the websocket) - fn start_received_messages_buffer_controller( - &self, - query_receiver: ReceivedBufferRequestReceiver, - mixnet_receiver: MixnetMessageReceiver, - reply_key_storage: SentReplyKeys, - reply_controller_sender: ReplyControllerSender, - shutdown: ShutdownListener, - ) { - info!("Starting received messages buffer controller..."); - ReceivedMessagesBufferController::new( - self.key_manager.encryption_keypair(), - query_receiver, - mixnet_receiver, - reply_key_storage, - reply_controller_sender, - ) - .start_with_shutdown(shutdown) - } - - async fn start_gateway_client( - &mut self, - mixnet_message_sender: MixnetMessageSender, - ack_sender: AcknowledgementSender, - shutdown: ShutdownListener, - ) -> 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(); @@ -215,158 +55,144 @@ 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 - } - - async fn setup_persistent_reply_storage( - &self, - shutdown: ShutdownListener, - ) -> Result { - // if the database file doesnt exist, initialise fresh storage, otherwise attempt to load the existing one - let db_path = self.config.get_base().get_reply_surb_database_path(); - let (persistent_storage, mem_store) = if db_path.exists() { - info!("loading existing surb database"); - let storage_backend = match fs_backend::Backend::try_load(db_path).await { - Ok(backend) => backend, - Err(err) => { - error!("failed to setup persistent storage backend for our reply needs: {err}"); - return Err(err.into()); - } - }; - let persistent_storage = PersistentReplyStorage::new(storage_backend); - let mem_store = persistent_storage.load_state_from_backend().await?; - (persistent_storage, mem_store) - } else { - info!("creating fresh surb database"); - let storage_backend = match fs_backend::Backend::init(db_path).await { - Ok(backend) => backend, - Err(err) => { - error!("failed to setup persistent storage backend for our reply needs: {err}"); - return Err(err.into()); - } - }; - let persistent_storage = PersistentReplyStorage::new(storage_backend); - let mem_store = CombinedReplyStorage::new( - self.config - .get_base() - .get_minimum_reply_surb_storage_threshold(), - self.config - .get_base() - .get_maximum_reply_surb_storage_threshold(), - ); - (persistent_storage, mem_store) - }; - - let store_clone = mem_store.clone(); - tokio::spawn(async move { - persistent_storage - .flush_on_shutdown(store_clone, shutdown) - .await - }); - - Ok(mem_store) + bandwidth_controller } fn start_websocket_listener( - &self, - buffer_requester: ReceivedBufferRequestSender, - msg_input: InputMessageSender, - shared_lane_queue_lengths: LaneQueueLengths, - client_connection_tx: ConnectionCommandSender, + config: &Config, + client_input: ClientInput, + client_output: ClientOutput, + self_address: Recipient, ) { info!("Starting websocket listener..."); + let ClientInput { + shared_lane_queue_lengths, + connection_command_sender, + input_sender, + } = client_input; + + let received_buffer_request_sender = client_output.received_buffer_request_sender; + let websocket_handler = websocket::Handler::new( - msg_input, - client_connection_tx, - buffer_requester, - &self.as_mix_recipient(), + input_sender, + connection_command_sender, + received_buffer_request_sender, + self_address, shared_lane_queue_lengths, ); - websocket::Listener::new(self.config.get_listening_port()).start(websocket_handler); + websocket::Listener::new(config.get_listening_port()).start(websocket_handler); } + /// blocking version of `start_socket` method. Will run forever (or until SIGINT is sent) + pub async fn run_socket_forever(self) -> Result<(), ClientError> { + let shutdown = self.start_socket().await?; + wait_for_signal().await; + + println!( + "Received signal - the client will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)." + ); + + log::info!("Sending shutdown"); + shutdown.signal_shutdown().ok(); + + // Some of these components have shutdown signalling implemented as part of socks5 work, + // but since it's not fully implemented (yet) for all the components of the native client, + // we don't try to wait and instead just stop immediately. + //log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); + //shutdown.wait_for_shutdown().await; + + log::info!("Stopping nym-client"); + Ok(()) + } + + pub async fn start_socket(self) -> Result { + if !self.config.get_socket_type().is_websocket() { + return Err(ClientError::InvalidSocketMode); + } + + let base_builder = BaseClientBuilder::new_from_base_config( + self.config.get_base(), + self.key_manager, + Some(Self::create_bandwidth_controller(&self.config).await), + non_wasm_helpers::setup_fs_reply_surb_backend( + self.config.get_base().get_reply_surb_database_path(), + self.config.get_debug_settings(), + ) + .await?, + ); + + let self_address = base_builder.as_mix_recipient(); + let mut started_client = base_builder.start_base().await?; + let client_input = started_client.client_input.register_producer(); + let client_output = started_client.client_output.register_consumer(); + + Self::start_websocket_listener(&self.config, client_input, client_output, self_address); + + info!("Client startup finished!"); + info!("The address of this client is: {}", self_address); + + Ok(started_client.shutdown_notifier) + } + + pub async fn start_direct(self) -> Result { + if self.config.get_socket_type().is_websocket() { + return Err(ClientError::InvalidSocketMode); + } + + let base_client = BaseClientBuilder::new_from_base_config( + self.config.get_base(), + self.key_manager, + Some(Self::create_bandwidth_controller(&self.config).await), + non_wasm_helpers::setup_fs_reply_surb_backend( + self.config.get_base().get_reply_surb_database_path(), + self.config.get_debug_settings(), + ) + .await?, + ); + + let mut started_client = base_client.start_base().await?; + let client_input = started_client.client_input.register_producer(); + let client_output = started_client.client_output.register_consumer(); + + // register our receiver + let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded(); + + // tell the buffer to start sending stuff to us + client_output + .received_buffer_request_sender + .unbounded_send(ReceivedBufferMessage::ReceiverAnnounce( + reconstructed_sender, + )) + .expect("the buffer request failed!"); + + Ok(DirectClient { + client_input, + reconstructed_receiver, + _shutdown_notifier: started_client.shutdown_notifier, + }) + } +} + +pub struct DirectClient { + client_input: ClientInput, + reconstructed_receiver: ReconstructedMessagesReceiver, + + // we need to keep reference to this guy otherwise things will start dropping + _shutdown_notifier: ShutdownNotifier, +} + +impl DirectClient { /// EXPERIMENTAL DIRECT RUST API /// It's untested and there are absolutely no guarantees about it (but seems to have worked /// well enough in local tests) @@ -374,9 +200,8 @@ impl NymClient { let lane = TransmissionLane::General; let input_msg = InputMessage::new_regular(recipient, message, lane); - self.input_tx - .as_ref() - .expect("start method was not called before!") + self.client_input + .input_sender .send(input_msg) .await .expect("InputMessageReceiver has stopped receiving!"); @@ -394,9 +219,8 @@ impl NymClient { let lane = TransmissionLane::General; let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane); - self.input_tx - .as_ref() - .expect("start method was not called before!") + self.client_input + .input_sender .send(input_msg) .await .expect("InputMessageReceiver has stopped receiving!"); @@ -409,9 +233,8 @@ impl NymClient { let lane = TransmissionLane::General; let input_msg = InputMessage::new_reply(recipient_tag, message, lane); - self.input_tx - .as_ref() - .expect("start method was not called before!") + self.client_input + .input_sender .send(input_msg) .await .expect("InputMessageReceiver has stopped receiving!"); @@ -426,151 +249,9 @@ impl NymClient { pub async fn wait_for_messages(&mut self) -> Vec { use futures::StreamExt; - self.receive_tx - .as_mut() - .expect("start method was not called before!") + self.reconstructed_receiver .next() .await .expect("buffer controller seems to have somehow died!") } - - /// blocking version of `start` method. Will run forever (or until SIGINT is sent) - pub async fn run_forever(&mut self) -> Result<(), ClientError> { - let mut shutdown = self.start().await?; - wait_for_signal().await; - - println!( - "Received signal - the client will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)." - ); - - log::info!("Sending shutdown"); - shutdown.signal_shutdown().ok(); - - // Some of these components have shutdown signalling implemented as part of socks5 work, - // but since it's not fully implemented (yet) for all the components of the native client, - // we don't try to wait and instead just stop immediately. - log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); - shutdown.wait_for_shutdown().await; - - log::info!("Stopping nym-client"); - Ok(()) - } - - pub async fn start(&mut self) -> Result { - info!("Starting nym client"); - // channels for inter-component communication - // TODO: make the channels be internally created by the relevant components - // rather than creating them here, so say for example the buffer controller would create the request channels - // and would allow anyone to clone the sender channel - - // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway - // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer - let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); - - // used for announcing connection or disconnection of a channel for pushing re-assembled messages to - let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); - - // channels responsible for controlling real messages - let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); - - // channels responsible for controlling ack messages - let (ack_sender, ack_receiver) = mpsc::unbounded(); - let shared_topology_accessor = TopologyAccessor::new(); - - // channels responsible for dealing with reply-related fun - let (reply_controller_sender, reply_controller_receiver) = - reply_controller::new_control_channels(); - - // Shutdown notifier for signalling tasks to stop - let shutdown = ShutdownNotifier::default(); - - let reply_storage = self - .setup_persistent_reply_storage(shutdown.subscribe()) - .await?; - - // 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_storage.key_storage(), - reply_controller_sender.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(), - ack_receiver, - input_receiver, - sphinx_message_sender.clone(), - reply_storage, - reply_controller_sender, - reply_controller_receiver, - shared_lane_queue_lengths.clone(), - client_connection_rx, - shutdown.subscribe(), - ); - - if !self - .config - .get_base() - .get_disabled_loop_cover_traffic_stream() - { - self.start_cover_traffic_stream( - shared_topology_accessor, - sphinx_message_sender, - shutdown.subscribe(), - ); - } - - match self.config.get_socket_type() { - SocketType::WebSocket => self.start_websocket_listener( - received_buffer_request_sender, - input_sender, - shared_lane_queue_lengths, - client_connection_tx, - ), - SocketType::None => { - // if we did not start the socket, it means we're running (supposedly) in the native mode - // and hence we should announce 'ourselves' to the buffer - let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded(); - - // tell the buffer to start sending stuff to us - received_buffer_request_sender - .unbounded_send(ReceivedBufferMessage::ReceiverAnnounce( - reconstructed_sender, - )) - .expect("the buffer request failed!"); - - self.receive_tx = Some(reconstructed_receiver); - self.input_tx = Some(input_sender); - } - } - - info!("Client startup finished!"); - info!("The address of this client is: {}", self.as_mix_recipient()); - - Ok(shutdown) - } } diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 6a38770eec..7fac2d6eba 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use clap::Args; -use client_core::{config::GatewayEndpoint, error::ClientCoreError}; +use client_core::client::replies::reply_storage::fs_backend; +use client_core::{config::GatewayEndpointConfig, error::ClientCoreError}; use config::NymConfig; use crate::{ @@ -46,6 +47,10 @@ pub(crate) struct Init { #[clap(long, hidden = true)] fastmode: bool, + /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) + #[clap(long, hidden = true)] + no_cover: bool, + /// Set this client to work in a enabled credentials mode that would attempt to use gateway /// with bandwidth credential requirement. #[cfg(feature = "coconut")] @@ -61,6 +66,7 @@ impl From for OverrideConfig { disable_socket: init_config.disable_socket, port: init_config.port, fastmode: init_config.fastmode, + no_cover: init_config.no_cover, #[cfg(feature = "coconut")] enabled_credentials_mode: init_config.enabled_credentials_mode, @@ -121,10 +127,12 @@ pub(crate) async fn execute(args: &Init) { ); println!("Client configuration completed."); - client_core::init::show_address(config.get_base()).unwrap_or_else(|err| { - eprintln!("Failed to show address\nError: {err}"); - std::process::exit(1) - }); + client_core::init::show_address::<_, fs_backend::Backend>(config.get_base()).unwrap_or_else( + |err| { + eprintln!("Failed to show address\nError: {err}"); + std::process::exit(1) + }, + ); } async fn setup_gateway( @@ -132,7 +140,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/mod.rs b/clients/native/src/commands/mod.rs index 648a1d6002..c3232165d8 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -80,6 +80,7 @@ pub(crate) struct OverrideConfig { disable_socket: bool, port: Option, fastmode: bool, + no_cover: bool, #[cfg(feature = "coconut")] enabled_credentials_mode: bool, @@ -141,6 +142,10 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi config.get_base_mut().set_high_default_traffic_volume(); } + if args.no_cover { + config.get_base_mut().set_no_cover_traffic(); + } + config } diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index d5c892fadc..4267fcbac5 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - client::{config::Config, NymClient}, + client::{config::Config, SocketClient}, commands::{override_config, OverrideConfig}, error::ClientError, }; @@ -39,6 +39,15 @@ pub(crate) struct Run { #[clap(short, long)] port: Option, + /// Mostly debug-related option to increase default traffic rate so that you would not need to + /// modify config post init + #[clap(long, hidden = true)] + fastmode: bool, + + /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) + #[clap(long, hidden = true)] + no_cover: bool, + /// Set this client to work in a enabled credentials mode that would attempt to use gateway /// with bandwidth credential requirement. #[cfg(feature = "coconut")] @@ -53,7 +62,8 @@ impl From for OverrideConfig { api_validators: run_config.api_validators, disable_socket: run_config.disable_socket, port: run_config.port, - fastmode: false, + fastmode: run_config.fastmode, + no_cover: run_config.no_cover, #[cfg(feature = "coconut")] enabled_credentials_mode: run_config.enabled_credentials_mode, } @@ -98,5 +108,5 @@ pub(crate) async fn execute(args: &Run) -> Result<(), ClientError> { return Err(ClientError::FailedLocalVersionCheck); } - NymClient::new(config).run_forever().await + SocketClient::new(config).run_socket_forever().await } diff --git a/clients/native/src/error.rs b/clients/native/src/error.rs index 43c88c24db..787b9a1bd1 100644 --- a/clients/native/src/error.rs +++ b/clients/native/src/error.rs @@ -1,29 +1,17 @@ -use client_core::client::replies::reply_storage::fs_backend::StorageError; +use client_core::client::replies::reply_storage::fs_backend; use client_core::error::ClientCoreError; -use crypto::asymmetric::identity::Ed25519RecoveryError; -use gateway_client::error::GatewayClientError; -use validator_client::ValidatorClientError; #[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), + ClientCoreError(#[from] ClientCoreError), + #[error("Failed to load config for: {0}")] FailedToLoadConfig(String), + #[error("Failed local version check, client and config mismatch")] FailedLocalVersionCheck, - #[error("experienced a failure with our reply surb persistent storage: {source}")] - SurbStorageError { - #[source] - #[from] - source: StorageError, - }, + + #[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 17a7e12b0b..a33a01edac 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -62,9 +62,13 @@ impl Clone for Handler { impl Drop for Handler { fn drop(&mut self) { - self.buffer_requester + if self + .buffer_requester .unbounded_send(ReceivedBufferMessage::ReceiverDisconnect) - .expect("the buffer request failed!") + .is_err() + { + error!("we failed to disconnect the receiver from the buffer! presumably the shutdown procedure has been initiated!") + } } } @@ -73,14 +77,14 @@ impl Handler { msg_input: InputMessageSender, client_connection_tx: ConnectionCommandSender, buffer_requester: ReceivedBufferRequestSender, - self_full_address: &Recipient, + self_full_address: Recipient, lane_queue_lengths: LaneQueueLengths, ) -> Self { Handler { msg_input, client_connection_tx, buffer_requester, - self_full_address: *self_full_address, + self_full_address, socket: None, received_response_type: Default::default(), lane_queue_lengths, diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index cff7f85a37..c2fba310b1 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/config/mod.rs b/clients/socks5/src/client/config/mod.rs index e58f13d013..2daa59035a 100644 --- a/clients/socks5/src/client/config/mod.rs +++ b/clients/socks5/src/client/config/mod.rs @@ -3,7 +3,7 @@ use crate::client::config::template::config_template; pub use client_core::config::MISSING_VALUE; -use client_core::config::{Config as BaseConfig, Debug}; +use client_core::config::{Config as BaseConfig, DebugConfig}; use config::defaults::DEFAULT_SOCKS5_LISTENING_PORT; use config::NymConfig; use nymsphinx::addressing::clients::Recipient; @@ -91,7 +91,7 @@ impl Config { &mut self.base } - pub fn get_debug_settings(&self) -> &Debug { + pub fn get_debug_settings(&self) -> &DebugConfig { self.get_base().get_debug_config() } diff --git a/clients/socks5/src/client/config/template.rs b/clients/socks5/src/client/config/template.rs index cdcdfbc082..527da38274 100644 --- a/clients/socks5/src/client/config/template.rs +++ b/clients/socks5/src/client/config/template.rs @@ -109,7 +109,7 @@ send_anonymously = {{ socks5.send_anonymously }} # The following options should not be modified unless you know EXACTLY what you are doing # as if set incorrectly, they may impact your anonymity. -[socks5_debug] +# [socks5_debug] [debug] diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 2d4866be05..d67e0caac6 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -8,43 +8,18 @@ use crate::socks::{ authentication::{AuthenticationMethods, Authenticator, User}, server::SphinxSocksServer, }; -use client_connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; -use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; -use client_core::client::inbound_messages::{ - InputMessage, InputMessageReceiver, InputMessageSender, +use client_core::client::base_client::{ + non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput, }; use client_core::client::key_manager::KeyManager; -use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController}; -use client_core::client::real_messages_control; -use client_core::client::real_messages_control::RealMessagesController; -use client_core::client::received_buffer::{ - ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, -}; -use client_core::client::replies::reply_controller; -use client_core::client::replies::reply_controller::{ - ReplyControllerReceiver, ReplyControllerSender, -}; -use client_core::client::replies::reply_storage::{ - fs_backend, CombinedReplyStorage, PersistentReplyStorage, SentReplyKeys, -}; -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 std::sync::atomic::Ordering; -use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; +use std::error::Error; +use task::{wait_for_signal_and_error, ShutdownListener, ShutdownNotifier}; pub mod config; @@ -78,131 +53,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, - ack_receiver: AcknowledgementReceiver, - input_receiver: InputMessageReceiver, - mix_sender: BatchMixMessageSender, - reply_storage: CombinedReplyStorage, - reply_controller_sender: ReplyControllerSender, - reply_controller_receiver: ReplyControllerReceiver, - client_connection_rx: ConnectionCommandReceiver, - lane_queue_lengths: LaneQueueLengths, - shutdown: ShutdownListener, - ) { - let mut controller_config = real_messages_control::Config::new( - self.config.get_debug_settings(), - self.key_manager.ack_key(), - 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_storage, - reply_controller_sender, - reply_controller_receiver, - lane_queue_lengths, - client_connection_rx, - ) - .start_with_shutdown(shutdown); - } - - // buffer controlling all messages fetched from provider - // required so that other components would be able to use them (say the websocket) - fn start_received_messages_buffer_controller( - &self, - query_receiver: ReceivedBufferRequestReceiver, - mixnet_receiver: MixnetMessageReceiver, - reply_key_storage: SentReplyKeys, - reply_controller_sender: ReplyControllerSender, - shutdown: ShutdownListener, - ) { - info!("Starting received messages buffer controller..."); - ReceivedMessagesBufferController::new( - self.key_manager.encryption_keypair(), - query_receiver, - mixnet_receiver, - reply_key_storage, - reply_controller_sender, - ) - .start_with_shutdown(shutdown) - } - - async fn start_gateway_client( - &mut self, - mixnet_message_sender: MixnetMessageSender, - ack_sender: AcknowledgementSender, - shutdown: ShutdownListener, - ) -> 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(); @@ -215,194 +66,97 @@ 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 - } - - async fn setup_persistent_reply_storage( - &self, - shutdown: ShutdownListener, - ) -> Result { - // if the database file doesnt exist, initialise fresh storage, otherwise attempt to load the existing one - let db_path = self.config.get_base().get_reply_surb_database_path(); - let (persistent_storage, mem_store) = if db_path.exists() { - info!("loading existing surb database"); - let storage_backend = match fs_backend::Backend::try_load(db_path).await { - Ok(backend) => backend, - Err(err) => { - error!("failed to setup persistent storage backend for our reply needs: {err}"); - return Err(err.into()); - } - }; - let persistent_storage = PersistentReplyStorage::new(storage_backend); - let mem_store = persistent_storage.load_state_from_backend().await?; - (persistent_storage, mem_store) - } else { - info!("creating fresh surb database"); - let storage_backend = match fs_backend::Backend::init(db_path).await { - Ok(backend) => backend, - Err(err) => { - error!("failed to setup persistent storage backend for our reply needs: {err}"); - return Err(err.into()); - } - }; - let persistent_storage = PersistentReplyStorage::new(storage_backend); - let mem_store = CombinedReplyStorage::new( - self.config - .get_base() - .get_minimum_reply_surb_storage_threshold(), - self.config - .get_base() - .get_maximum_reply_surb_storage_threshold(), - ); - (persistent_storage, mem_store) - }; - - let store_clone = mem_store.clone(); - tokio::spawn(async move { - persistent_storage - .flush_on_shutdown(store_clone, shutdown) - .await - }); - - Ok(mem_store) + bandwidth_controller } fn start_socks5_listener( - &self, - buffer_requester: ReceivedBufferRequestSender, - msg_input: InputMessageSender, - client_connection_tx: ConnectionCommandSender, - lane_queue_lengths: LaneQueueLengths, + config: &Config, + client_input: ClientInput, + client_output: ClientOutput, + self_address: Recipient, shutdown: ShutdownListener, ) { info!("Starting socks5 listener..."); let auth_methods = vec![AuthenticationMethods::NoAuth as u8]; let allowed_users: Vec = Vec::new(); + let ClientInput { + shared_lane_queue_lengths, + connection_command_sender, + input_sender, + } = client_input; + + let received_buffer_request_sender = client_output.received_buffer_request_sender; + let authenticator = Authenticator::new(auth_methods, allowed_users); let mut sphinx_socks = SphinxSocksServer::new( - self.config.get_listening_port(), + config.get_listening_port(), authenticator, - self.config.get_provider_mix_address(), - self.as_mix_recipient(), - lane_queue_lengths, + config.get_provider_mix_address(), + self_address, + shared_lane_queue_lengths, socks::client::Config::new( - self.config.get_send_anonymously(), - self.config.get_connection_start_surbs(), - self.config.get_per_request_surbs(), + config.get_send_anonymously(), + config.get_connection_start_surbs(), + config.get_per_request_surbs(), ), + shutdown.clone(), + ); + task::spawn_with_report_error( + async move { + sphinx_socks + .serve( + input_sender, + received_buffer_request_sender, + connection_command_sender, + ) + .await + }, shutdown, ); - tokio::spawn(async move { - sphinx_socks - .serve(msg_input, buffer_requester, client_connection_tx) - .await - }); } /// 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 { @@ -413,126 +167,56 @@ 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()) - // .expect("Failed to load reply key storage!"); - - // channels responsible for dealing with reply-related fun - let (reply_controller_sender, reply_controller_receiver) = - reply_controller::new_control_channels(); - - // Shutdown notifier for signalling tasks to stop - let shutdown = ShutdownNotifier::default(); - - let reply_storage = self - .setup_persistent_reply_storage(shutdown.subscribe()) - .await?; - - // 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_storage.key_storage(), - reply_controller_sender.clone(), - shutdown.subscribe(), + pub async fn start(self) -> Result { + let base_builder = BaseClientBuilder::new_from_base_config( + self.config.get_base(), + self.key_manager, + Some(Self::create_bandwidth_controller(&self.config).await), + non_wasm_helpers::setup_fs_reply_surb_backend( + self.config.get_base().get_reply_surb_database_path(), + self.config.get_debug_settings(), + ) + .await?, ); - let gateway_client = self - .start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe()) - .await; + let self_address = base_builder.as_mix_recipient(); + let mut started_client = base_builder.start_base().await?; + let client_input = started_client.client_input.register_producer(); + let client_output = started_client.client_output.register_consumer(); - // The sphinx_message_sender is the transmitter for any component generating sphinx packets - // that are to be sent to the mixnet. They are used by cover traffic stream and real - // traffic stream. - // The MixTrafficController then sends the actual traffic - let sphinx_message_sender = - Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe()); - - // Channel for announcing closed (socks5) connections by the controller. - // This will be forwarded to `OutQueueControl` - let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); - - // Shared queue length data. Published by the `OutQueueController` in the client, and used - // primarily to throttle incoming connections - let shared_lane_queue_lengths = LaneQueueLengths::new(); - - self.start_real_traffic_controller( - shared_topology_accessor.clone(), - ack_receiver, - input_receiver, - sphinx_message_sender.clone(), - reply_storage, - reply_controller_sender, - reply_controller_receiver, - 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 1329ee8d01..692d91fd20 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use clap::Args; -use client_core::{config::GatewayEndpoint, error::ClientCoreError}; +use client_core::client::replies::reply_storage::fs_backend; +use client_core::{config::GatewayEndpointConfig, error::ClientCoreError}; use config::NymConfig; use crate::{ @@ -54,6 +55,10 @@ pub(crate) struct Init { #[clap(long, hidden = true)] fastmode: bool, + /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) + #[clap(long, hidden = true)] + no_cover: bool, + /// Set this client to work in a enabled credentials mode that would attempt to use gateway /// with bandwidth credential requirement. #[cfg(feature = "coconut")] @@ -69,7 +74,7 @@ impl From for OverrideConfig { port: init_config.port, use_anonymous_sender_tag: init_config.use_anonymous_sender_tag, fastmode: init_config.fastmode, - + no_cover: init_config.no_cover, #[cfg(feature = "coconut")] enabled_credentials_mode: init_config.enabled_credentials_mode, } @@ -130,10 +135,12 @@ pub(crate) async fn execute(args: &Init) { ); println!("Client configuration completed."); - client_core::init::show_address(config.get_base()).unwrap_or_else(|err| { - eprintln!("Failed to show address\nError: {err}"); - std::process::exit(1) - }); + client_core::init::show_address::<_, fs_backend::Backend>(config.get_base()).unwrap_or_else( + |err| { + eprintln!("Failed to show address\nError: {err}"); + std::process::exit(1) + }, + ); } async fn setup_gateway( @@ -141,7 +148,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 7489329dcb..576a84f177 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}; @@ -83,12 +84,13 @@ pub(crate) struct OverrideConfig { port: Option, use_anonymous_sender_tag: bool, fastmode: bool, + no_cover: bool, #[cfg(feature = "coconut")] enabled_credentials_mode: bool, } -pub(crate) async fn execute(args: &Cli) -> Result<(), Socks5ClientError> { +pub(crate) async fn execute(args: &Cli) -> Result<(), Box> { let bin_name = "nym-socks5-client"; match &args.command { @@ -140,6 +142,10 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi config.get_base_mut().set_high_default_traffic_volume(); } + if args.no_cover { + config.get_base_mut().set_no_cover_traffic(); + } + config } diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index 0938bfe00e..7a2a97d921 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -51,6 +51,15 @@ pub(crate) struct Run { #[clap(short, long)] port: Option, + /// Mostly debug-related option to increase default traffic rate so that you would not need to + /// modify config post init + #[clap(long, hidden = true)] + fastmode: bool, + + /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) + #[clap(long, hidden = true)] + no_cover: bool, + /// Set this client to work in a enabled credentials mode that would attempt to use gateway /// with bandwidth credential requirement. #[cfg(feature = "coconut")] @@ -65,8 +74,8 @@ impl From for OverrideConfig { api_validators: run_config.api_validators, port: run_config.port, use_anonymous_sender_tag: run_config.use_anonymous_sender_tag, - fastmode: false, - + fastmode: run_config.fastmode, + no_cover: run_config.no_cover, #[cfg(feature = "coconut")] enabled_credentials_mode: run_config.enabled_credentials_mode, } @@ -95,14 +104,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(), + ))); } }; @@ -111,7 +122,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 0fb5475bb1..23cd715cf5 100644 --- a/clients/socks5/src/error.rs +++ b/clients/socks5/src/error.rs @@ -1,29 +1,24 @@ -use client_core::client::replies::reply_storage::fs_backend::StorageError; +use crate::socks::types::SocksProxyError; +use client_core::client::replies::reply_storage::fs_backend; use client_core::error::ClientCoreError; -use crypto::asymmetric::identity::Ed25519RecoveryError; -use gateway_client::error::GatewayClientError; -use validator_client::ValidatorClientError; #[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), + ClientCoreError(#[from] ClientCoreError), + + #[error("SOCKS proxy error")] + SocksProxyError(SocksProxyError), + #[error("Failed to load config for: {0}")] FailedToLoadConfig(String), + #[error("Failed local version check, client and config mismatch")] FailedLocalVersionCheck, - #[error("experienced a failure with our reply surb persistent storage: {source}")] - SurbStorageError { - #[source] - #[from] - source: StorageError, - }, + + #[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 0b4c07b597..3b35a4caf9 100644 --- a/clients/socks5/src/socks/client.rs +++ b/clients/socks5/src/socks/client.rs @@ -2,8 +2,8 @@ use super::authentication::{AuthenticationMethods, Authenticator, User}; use super::request::{SocksCommand, SocksRequest}; -use super::types::{ResponseCode, SocksProxyError}; -use super::{RESERVED, SOCKS_VERSION}; +use super::types::{ResponseCodeV4, ResponseCodeV5, SocksProxyError}; +use super::{SocksVersion, RESERVED, SOCKS4_VERSION, SOCKS5_VERSION}; use client_connections::{LaneQueueLengths, TransmissionLane}; use client_core::client::inbound_messages::{InputMessage, InputMessageSender}; use futures::channel::mpsc; @@ -150,14 +150,14 @@ impl Config { /// A client connecting to the Socks proxy server, because /// it wants to make a Nym-protected outbound request. Typically, this is /// something like e.g. a wallet app running on your laptop connecting to -/// SphinxSocksServer. +/// `SphinxSocksServer`. pub(crate) struct SocksClient { config: Config, controller_sender: ControllerSender, stream: StreamState, auth_nmethods: u8, authenticator: Authenticator, - socks_version: u8, + socks_version: Option, input_sender: InputMessageSender, connection_id: ConnectionId, service_provider: Recipient, @@ -180,31 +180,34 @@ impl Drop for SocksClient { } impl SocksClient { - /// Create a new SOCKClient #[allow(clippy::too_many_arguments)] pub fn new( config: Config, stream: TcpStream, authenticator: Authenticator, input_sender: InputMessageSender, - service_provider: Recipient, + service_provider: &Recipient, controller_sender: ControllerSender, - self_address: Recipient, + self_address: &Recipient, lane_queue_lengths: LaneQueueLengths, - 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 { config, controller_sender, connection_id, stream: StreamState::Available(stream), auth_nmethods: 0, - socks_version: 0, + socks_version: None, authenticator, input_sender, - service_provider, - self_address, + service_provider: *service_provider, + self_address: *self_address, started_proxy: false, lane_queue_lengths, shutdown_listener, @@ -216,16 +219,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(()) } @@ -233,25 +269,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_anonymous_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { @@ -299,10 +337,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(); let anonymous = self.config.use_surbs_for_responses; @@ -344,8 +387,17 @@ impl SocksClient { async fn handle_request(&mut self) -> Result<(), SocksProxyError> { debug!("Handling CONNECT Command"); - let request = SocksRequest::from_stream(&mut self.stream).await?; - let remote_address = request.to_string(); + let version = self + .socks_version + .as_ref() + .expect("Must read version before parsing request"); + + let request = match version { + SocksVersion::V4 => SocksRequest::from_stream_socks4(&mut self.stream).await?, + SocksVersion::V5 => SocksRequest::from_stream_socks5(&mut self.stream).await?, + }; + + let remote_address = request.address_string(); // setup for receiving from the mixnet let (mix_sender, mix_receiver) = mpsc::unbounded(); @@ -354,7 +406,10 @@ impl SocksClient { // Use the Proxy to connect to the specified addr/port SocksCommand::Connect => { trace!("Connecting to: {:?}", remote_address.clone()); - self.acknowledge_socks5().await; + match version { + SocksVersion::V4 => self.acknowledge_socks4().await, + SocksVersion::V5 => self.acknowledge_socks5().await, + } self.started_proxy = true; self.controller_sender @@ -385,8 +440,8 @@ impl SocksClient { async fn acknowledge_socks5(&mut self) { self.stream .write_all(&[ - SOCKS_VERSION, - ResponseCode::Success as u8, + SOCKS5_VERSION, + ResponseCodeV5::Success as u8, RESERVED, 1, 127, @@ -400,13 +455,30 @@ impl SocksClient { .unwrap(); } + /// Writes a Socks4 header back to the requesting client's TCP stream, + async fn acknowledge_socks4(&mut self) { + self.stream + .write_all(&[ + 0, //SOCKS4_VERSION, + ResponseCodeV4::Granted as u8, + 0, + 0, + 127, + 0, + 0, + 1, + ]) + .await + .unwrap(); + } + /// Authenticate the incoming request. Each request is checked for its /// authentication method. A user/password request will extract the /// username and password from the stream, then check with the Authenticator /// to see if the resulting user is allowed. /// /// A lot of this could probably be put into the `SocksRequest::from_stream()` - /// constructor, and/or cleaned up with tokio::codec. It's mostly just + /// constructor, and/or cleaned up with `tokio::codec`. It's mostly just /// read-a-byte-or-two. The bytes being extracted look like this: /// /// +----+------+----------+------+------------+ @@ -418,7 +490,7 @@ impl SocksClient { /// Pulling out the stream code into its own home, and moving the if/else logic /// into the Authenticator (where it'll be more easily testable) /// would be a good next step. - async fn authenticate(&mut self) -> Result<(), SocksProxyError> { + async fn authenticate_socks5(&mut self) -> Result<(), SocksProxyError> { debug!("Authenticating w/ {}", self.stream.peer_addr()?.ip()); // Get valid auth methods let methods = self.get_available_methods().await?; @@ -427,7 +499,7 @@ impl SocksClient { let mut response = [0u8; 2]; // Set the version in the response - response[0] = SOCKS_VERSION; + response[0] = SOCKS5_VERSION; if methods.contains(&(AuthenticationMethods::UserPass as u8)) { // Set the default auth method (NO AUTH) response[1] = AuthenticationMethods::UserPass as u8; @@ -463,11 +535,11 @@ impl SocksClient { // Authenticate passwords if self.authenticator.is_allowed(&user) { debug!("Access Granted. User: {}", user.username); - let response = [1, ResponseCode::Success as u8]; + let response = [1, ResponseCodeV5::Success as u8]; self.stream.write_all(&response).await?; } else { debug!("Access Denied. User: {}", user.username); - let response = [1, ResponseCode::Failure as u8]; + let response = [1, ResponseCodeV5::Failure as u8]; self.stream.write_all(&response).await?; // Shutdown @@ -486,7 +558,7 @@ impl SocksClient { response[1] = AuthenticationMethods::NoMethods as u8; self.stream.write_all(&response).await?; self.shutdown().await?; - Err(ResponseCode::Failure.into()) + Err(ResponseCodeV5::Failure.into()) } } diff --git a/clients/socks5/src/socks/mixnet_responses.rs b/clients/socks5/src/socks/mixnet_responses.rs index b59a805583..7613a32019 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 236e5391ec..16603839ff 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; pub(crate) 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 010c605154..0bba59ee0b 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 crate::socks::client; use client_connections::{ConnectionCommandSender, LaneQueueLengths}; @@ -13,6 +12,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; @@ -60,8 +60,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 @@ -87,48 +89,27 @@ impl SphinxSocksServer { loop { tokio::select! { Ok((stream, _remote)) = listener.accept() => { - // TODO Optimize this let mut client = SocksClient::new( self.client_config, stream, self.authenticator.clone(), input_sender.clone(), - self.service_provider, + &self.service_provider, controller_sender.clone(), - self.self_address, + &self.self_address, self.lane_queue_lengths.clone(), self.shutdown.clone(), ); tokio::spawn(async move { - { - match client.run().await { - Ok(_) => {} - Err(error) => { - error!("Error! {}", error); - let error_text = format!("{}", error); - - let response: ResponseCode; - - if error_text.contains("Host") { - response = ResponseCode::HostUnreachable; - } else if error_text.contains("Network") { - response = ResponseCode::NetworkUnreachable; - } else if error_text.contains("ttl") { - response = ResponseCode::TtlExpired - } else { - response = ResponseCode::Failure - } - - if client.error(response).await.is_err() { - warn!("Failed to send error code"); - }; - if client.shutdown().await.is_err() { - warn!("Failed to shutdown TcpStream"); - }; - } + if let Err(err) = client.run().await { + error!("Error! {}", err); + if client.send_error(err).await.is_err() { + warn!("Failed to send error code"); + }; + if client.shutdown().await.is_err() { + warn!("Failed to shutdown TcpStream"); }; - // client gets dropped here } }); }, diff --git a/clients/socks5/src/socks/types.rs b/clients/socks5/src/socks/types.rs index b043da5ccc..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 f76918463d..f16584c5a3 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 7854c01627..4b5158bbc7 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -2,56 +2,46 @@ // SPDX-License-Identifier: Apache-2.0 use self::config::Config; -use client_connections::{ConnectionCommandReceiver, LaneQueueLengths, TransmissionLane}; -use client_core::client::replies::reply_controller; -use client_core::client::replies::reply_controller::{ - ReplyControllerReceiver, ReplyControllerSender, -}; -use client_core::client::replies::reply_storage::{CombinedReplyStorage, SentReplyKeys}; -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::replies::reply_storage::browser_backend; +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 below 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, + storage_backend: 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] @@ -59,15 +49,21 @@ impl NymClient { #[wasm_bindgen(constructor)] pub fn new(config: Config) -> Self { Self { + storage_backend: Some(Self::setup_reply_surb_storage_backend(&config)), 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; @@ -76,6 +72,15 @@ impl NymClient { KeyManager::new(&mut rng) } + // don't get too excited about the name, under the hood it's just a big fat placeholder + // with no persistence + fn setup_reply_surb_storage_backend(config: &Config) -> browser_backend::Backend { + browser_backend::Backend::new( + config.debug.minimum_reply_surb_storage_threshold, + config.debug.maximum_reply_surb_storage_threshold, + ) + } + pub fn set_on_message(&mut self, on_message: js_sys::Function) { self.on_message = Some(on_message); } @@ -89,325 +94,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).into()); + if let Some(address) = &self.self_address { + address.clone() + } else { + self.as_mix_recipient().to_string() } - - stream.start(); - } - - #[allow(clippy::too_many_arguments)] - fn start_real_traffic_controller( - &self, - topology_accessor: TopologyAccessor, - ack_receiver: AcknowledgementReceiver, - input_receiver: InputMessageReceiver, - mix_sender: BatchMixMessageSender, - reply_storage: CombinedReplyStorage, - reply_controller_sender: ReplyControllerSender, - reply_controller_receiver: ReplyControllerReceiver, - lane_queue_lengths: LaneQueueLengths, - client_connection_rx: ConnectionCommandReceiver, - ) { - let mut controller_config = real_messages_control::Config::new( - &self.config.debug, - self.key_manager.ack_key(), - self.as_mix_recipient(), - ); - - if let Some(size) = &self.config.debug.use_extended_packet_size { - controller_config.set_custom_packet_size((*size).into()); - } - - console_log!("Starting real traffic stream..."); - - RealMessagesController::new( - controller_config, - ack_receiver, - input_receiver, - mix_sender, - topology_accessor, - reply_storage, - reply_controller_sender, - reply_controller_receiver, - lane_queue_lengths, - client_connection_rx, - ) - .start(); - } - - // 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: SentReplyKeys, - reply_controller_sender: ReplyControllerSender, - ) { - console_log!("Starting received messages buffer controller..."); - ReceivedMessagesBufferController::new( - self.key_manager.encryption_keypair(), - query_receiver, - mixnet_receiver, - reply_key_storage, - reply_controller_sender, - ) - .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.sender_tag.is_some() { - console_log!("the received message contained a sender_tag meaning it also contained some reply surbs, but we do not know how to handle them (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(); - - // channels responsible for dealing with reply-related fun - let (reply_controller_sender, reply_controller_receiver) = - reply_controller::new_control_channels(); - - // ===================== - // ===================== - // ======TEMPORARY====== - // ===================== - // ===================== - // TODO: lower the value and improve the reliability when it's low (because it should still work in that case) - // (it goes insane at 10,200) - let reply_storage = CombinedReplyStorage::new( - self.config.debug.minimum_reply_surb_storage_threshold, - self.config.debug.maximum_reply_surb_storage_threshold, - ); - - // 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, - reply_storage.key_storage(), - reply_controller_sender.clone(), - ); - - 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(), - reply_storage, - reply_controller_sender, - reply_controller_receiver, - shared_lane_queue_lengths, - client_connection_rx, - ); - - 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 @@ -427,13 +133,66 @@ impl NymClient { let input_msg = InputMessage::new_regular(recipient, message, 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, + self.storage_backend.take().unwrap(), + 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..26d77482db --- /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.sender_tag.is_some() { + console_log!("the received message contained a sender tag (meaning we also got some surbs!), but we do not know how to handle that (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 bac92ec092..3bd5bbf19b 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -10,11 +10,11 @@ pub use crate::packet_router::{ use crate::socket_state::{PartiallyDelegated, SocketState}; use crate::{cleanup_socket_message, try_decrypt_binary_message}; use crypto::asymmetric::identity; -use futures::{FutureExt, SinkExt, StreamExt}; +use futures::{SinkExt, StreamExt}; use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; use gateway_requests::iv::IV; use gateway_requests::registration::handshake::{client_handshake, SharedKeys}; -use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse}; +use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse, PROTOCOL_VERSION}; use log::*; use network_defaults::{REMAINING_BANDWIDTH_THRESHOLD, TOKENS_TO_BURN}; use nymsphinx::forwarding::packet::MixPacket; @@ -22,6 +22,7 @@ use rand::rngs::OsRng; use std::convert::TryFrom; use std::sync::Arc; use std::time::Duration; +use task::ShutdownListener; use tungstenite::protocol::Message; #[cfg(feature = "coconut")] @@ -30,8 +31,6 @@ use coconut_interface::Credential; #[cfg(not(target_arch = "wasm32"))] use credential_storage::PersistentStorage; #[cfg(not(target_arch = "wasm32"))] -use task::ShutdownListener; -#[cfg(not(target_arch = "wasm32"))] use tokio_tungstenite::connect_async; #[cfg(target_arch = "wasm32")] @@ -67,9 +66,8 @@ pub struct GatewayClient { /// Delay between each subsequent reconnection attempt. reconnection_backoff: Duration, - #[cfg(not(target_arch = "wasm32"))] /// Listen to shutdown messages. - shutdown: Option, + shutdown: ShutdownListener, } impl GatewayClient { @@ -85,7 +83,7 @@ impl GatewayClient { ack_sender: AcknowledgementSender, response_timeout_duration: Duration, bandwidth_controller: Option>, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, + shutdown: ShutdownListener, ) -> Self { GatewayClient { authenticated: false, @@ -97,18 +95,12 @@ impl GatewayClient { local_identity, shared_key, connection: SocketState::NotConnected, - packet_router: PacketRouter::new( - ack_sender, - mixnet_message_sender, - #[cfg(not(target_arch = "wasm32"))] - shutdown.clone(), - ), + packet_router: PacketRouter::new(ack_sender, mixnet_message_sender, shutdown.clone()), response_timeout_duration, bandwidth_controller, should_reconnect_on_failure: true, reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF, - #[cfg(not(target_arch = "wasm32"))] shutdown, } } @@ -136,7 +128,6 @@ impl GatewayClient { gateway_owner: String, local_identity: Arc, response_timeout_duration: Duration, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, ) -> Self { use futures::channel::mpsc; @@ -144,12 +135,8 @@ impl GatewayClient { // perfectly fine here, because it's not meant to be used let (ack_tx, _) = mpsc::unbounded(); let (mix_tx, _) = mpsc::unbounded(); - let packet_router = PacketRouter::new( - ack_tx, - mix_tx, - #[cfg(not(target_arch = "wasm32"))] - shutdown.clone(), - ); + let shutdown = ShutdownListener::dummy(); + let packet_router = PacketRouter::new(ack_tx, mix_tx, shutdown.clone()); GatewayClient { authenticated: false, @@ -167,7 +154,6 @@ impl GatewayClient { should_reconnect_on_failure: false, reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF, - #[cfg(not(target_arch = "wasm32"))] shutdown, } } @@ -295,42 +281,19 @@ impl GatewayClient { // technically the `wasm_timer` also works outside wasm, but unless required, // I really prefer to just stick to tokio #[cfg(target_arch = "wasm32")] - let timeout = wasm_timer::Delay::new(self.response_timeout_duration); - - let mut fused_timeout = timeout.fuse(); - let mut fused_stream = conn.fuse(); - - // Bit of an ugly workaround for selecting on an `Option` without having access to - // `tokio::select` - #[cfg(not(target_arch = "wasm32"))] - let shutdown = { - let m_shutdown = self.shutdown.clone(); - async { - if let Some(mut s) = m_shutdown { - s.recv().await - } else { - std::future::pending::<()>().await - } - } - .fuse() - }; - #[cfg(not(target_arch = "wasm32"))] - tokio::pin!(shutdown); - - #[cfg(target_arch = "wasm32")] - let mut shutdown = std::future::pending::<()>().fuse(); + let mut timeout = wasm_timer::Delay::new(self.response_timeout_duration); loop { - futures::select! { - _ = shutdown => { + tokio::select! { + _ = self.shutdown.recv() => { log::trace!("GatewayClient control response: Received shutdown"); log::debug!("GatewayClient control response: Exiting"); break Err(GatewayClientError::ConnectionClosedGatewayShutdown); } - _ = &mut fused_timeout => { + _ = &mut timeout => { break Err(GatewayClientError::Timeout); } - msg = fused_stream.next() => { + msg = conn.next() => { let ws_msg = match cleanup_socket_message(msg) { Err(err) => break Err(err), Ok(msg) => msg @@ -401,13 +364,10 @@ impl GatewayClient { .batch_send_without_response(messages) .await { - error!("failed to batch send messages - {}...", err); + error!("failed to batch send messages - {err}..."); // we must ensure we do not leave the task still active if let Err(err) = self.recover_socket_connection().await { - error!( - "... and the delegated stream has also errored out - {}", - err - ) + error!("... and the delegated stream has also errored out - {err}") } Err(err) } else { @@ -427,13 +387,10 @@ impl GatewayClient { SocketState::Available(ref mut conn) => Ok(conn.send(msg).await?), SocketState::PartiallyDelegated(ref mut partially_delegated) => { if let Err(err) = partially_delegated.send_without_response(msg).await { - error!("failed to send message without response - {}...", err); + error!("failed to send message without response - {err}..."); // we must ensure we do not leave the task still active if let Err(err) = self.recover_socket_connection().await { - error!( - "... and the delegated stream has also errored out - {}", - err - ) + error!("... and the delegated stream has also errored out - {err}") } Err(err) } else { @@ -445,6 +402,27 @@ impl GatewayClient { } } + fn check_gateway_protocol( + &self, + gateway_protocol: Option, + ) -> Result<(), GatewayClientError> { + // right now there are no failure cases here, but this might change in the future + match gateway_protocol { + Some(v) if v == PROTOCOL_VERSION => { + info!("the gateway is using exactly the same protocol version as we are. We're good to continue!"); + Ok(()) + } + v => { + let err = GatewayClientError::IncompatibleProtocol { + gateway: v, + current: PROTOCOL_VERSION, + }; + error!("{err}"); + Err(err) + } + } + } + async fn register(&mut self) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); @@ -467,11 +445,20 @@ impl GatewayClient { .map_err(GatewayClientError::RegistrationFailure), _ => unreachable!(), }?; - self.authenticated = match self.read_control_response().await? { - ServerResponse::Register { status } => Ok(status), - ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - _ => Err(GatewayClientError::UnexpectedResponse), - }?; + let (authentication_status, gateway_protocol) = match self.read_control_response().await? { + ServerResponse::Register { + protocol_version, + status, + } => (status, protocol_version), + ServerResponse::Error { message } => { + return Err(GatewayClientError::GatewayError(message)) + } + _ => return Err(GatewayClientError::UnexpectedResponse), + }; + + self.check_gateway_protocol(gateway_protocol)?; + self.authenticated = authentication_status; + if self.authenticated { self.shared_key = Some(Arc::new(shared_key)); } @@ -510,9 +497,11 @@ impl GatewayClient { match self.send_websocket_message(msg).await? { ServerResponse::Authenticate { + protocol_version, status, bandwidth_remaining, } => { + self.check_gateway_protocol(protocol_version)?; self.authenticated = status; self.bandwidth_remaining = bandwidth_remaining; Ok(()) @@ -748,7 +737,6 @@ impl GatewayClient { .as_ref() .expect("no shared key present even though we're authenticated!"), ), - #[cfg(not(target_arch = "wasm32"))] self.shutdown.clone(), ) } diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index bb6f71f564..8a8104658b 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -85,6 +85,9 @@ pub enum GatewayClientError { #[error("Failed to send mixnet message")] MixnetMsgSenderFailedToSend, + + #[error("Attempted to negotiate connection with gateway using incompatible protocol version. Ours is {current} and the gateway reports {gateway:?}")] + IncompatibleProtocol { gateway: Option, current: u8 }, } impl GatewayClientError { diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index 6732d2202e..fd63077ee6 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -4,15 +4,13 @@ // JS: I personally don't like this name very much, but could not think of anything better. // I will gladly take any suggestions on how to rename this. +use crate::error::GatewayClientError; use futures::channel::mpsc; use log::*; use nymsphinx::addressing::nodes::MAX_NODE_ADDRESS_UNPADDED_LEN; use nymsphinx::params::packet_sizes::PacketSize; -#[cfg(not(target_arch = "wasm32"))] use task::ShutdownListener; -use crate::error::GatewayClientError; - pub type MixnetMessageSender = mpsc::UnboundedSender>>; pub type MixnetMessageReceiver = mpsc::UnboundedReceiver>>; @@ -23,20 +21,18 @@ pub type AcknowledgementReceiver = mpsc::UnboundedReceiver>>; pub struct PacketRouter { ack_sender: AcknowledgementSender, mixnet_message_sender: MixnetMessageSender, - #[cfg(not(target_arch = "wasm32"))] - shutdown: Option, + shutdown: ShutdownListener, } impl PacketRouter { pub fn new( ack_sender: AcknowledgementSender, mixnet_message_sender: MixnetMessageSender, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, + shutdown: ShutdownListener, ) -> Self { PacketRouter { ack_sender, mixnet_message_sender, - #[cfg(not(target_arch = "wasm32"))] shutdown, } } @@ -86,13 +82,10 @@ impl PacketRouter { if !received_messages.is_empty() { trace!("routing 'real'"); if let Err(err) = self.mixnet_message_sender.unbounded_send(received_messages) { - #[cfg(not(target_arch = "wasm32"))] - if let Some(shutdown) = &mut self.shutdown { - if shutdown.is_shutdown_poll() { - // This should ideally not happen, but it's ok - log::warn!("Failed to send mixnet message due to receiver task shutdown"); - return Err(GatewayClientError::MixnetMsgSenderFailedToSend); - } + if self.shutdown.is_shutdown_poll() || self.shutdown.is_dummy() { + // This should ideally not happen, but it's ok + log::warn!("Failed to send mixnet message due to receiver task shutdown"); + return Err(GatewayClientError::MixnetMsgSenderFailedToSend); } // This should never happen during ordinary operation the way it's currently used. // Abort to be on the safe side diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 2d66e782e8..aa96b9a4fa 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -10,7 +10,6 @@ use futures::{SinkExt, StreamExt}; use gateway_requests::registration::handshake::SharedKeys; use log::*; use std::sync::Arc; -#[cfg(not(target_arch = "wasm32"))] use task::ShutdownListener; use tungstenite::Message; @@ -85,7 +84,7 @@ impl PartiallyDelegated { conn: WsConn, packet_router: PacketRouter, shared_key: Arc, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, + mut shutdown: ShutdownListener, ) -> Self { // when called for, it NEEDS TO yield back the stream so that we could merge it and // read control request responses. @@ -99,27 +98,9 @@ impl PartiallyDelegated { let mut chunk_stream = (&mut stream).ready_chunks(8); let mut packet_router = packet_router; - // Bit of an ugly workaround for selecting on an `Option` without having access to - // `tokio::select` - #[cfg(not(target_arch = "wasm32"))] - let shutdown = { - async { - if let Some(mut s) = shutdown { - s.recv().await - } else { - std::future::pending::<()>().await - } - } - }; - #[cfg(not(target_arch = "wasm32"))] - tokio::pin!(shutdown); - - #[cfg(target_arch = "wasm32")] - let mut shutdown = std::future::pending::<()>(); - let ret_err = loop { tokio::select! { - _ = &mut shutdown => { + _ = shutdown.recv() => { log::trace!("GatewayClient listener: Received shutdown"); log::debug!("GatewayClient listener: Exiting"); return; @@ -142,7 +123,10 @@ impl PartiallyDelegated { if match ret_err { Err(err) => stream_sender.send(Err(err)), - Ok(_) => stream_sender.send(Ok(stream)), + Ok(_) => { + shutdown.mark_as_success(); + stream_sender.send(Ok(stream)) + } } .is_err() { diff --git a/common/client-libs/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/client-libs/validator-client/src/validator_api/mod.rs b/common/client-libs/validator-client/src/validator_api/mod.rs index 4a7b7af3cd..3cb7ae2ce9 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -136,7 +136,12 @@ impl Client { &self, ) -> Result, ValidatorAPIError> { self.query_validator_api( - &[routes::API_VERSION, routes::MIXNODES, routes::DETAILED], + &[ + routes::API_VERSION, + routes::STATUS, + routes::MIXNODES, + routes::DETAILED, + ], NO_PARAMS, ) .await @@ -161,6 +166,7 @@ impl Client { self.query_validator_api( &[ routes::API_VERSION, + routes::STATUS, routes::MIXNODES, routes::ACTIVE, routes::DETAILED, @@ -252,6 +258,7 @@ impl Client { self.query_validator_api( &[ routes::API_VERSION, + routes::STATUS, routes::MIXNODES, routes::REWARDED, routes::DETAILED, diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index 35b38512fa..ce0195cce4 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -22,6 +22,7 @@ thiserror = "1" time = { version = "0.3.6", features = ["parsing", "formatting"] } toml = "0.5.6" url = "2.2" +tap = "1" cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" } cosmwasm-std = { version = "1.0.0" } diff --git a/common/commands/src/context/errors.rs b/common/commands/src/context/errors.rs index 5c3d24a2bb..44a99f49ed 100644 --- a/common/commands/src/context/errors.rs +++ b/common/commands/src/context/errors.rs @@ -15,4 +15,10 @@ pub enum ContextError { // TODO: improve this to return known errors #[error("failed to create client - {0}")] NymdError(String), + + #[error("{0}")] + NymdErrorPassthrough(#[from] validator_client::nymd::error::NymdError), + + #[error("{0}")] + ValidatorClientError(#[from] validator_client::ValidatorClientError), } diff --git a/common/commands/src/context/mod.rs b/common/commands/src/context/mod.rs index 856a03e62a..4f55aa1847 100644 --- a/common/commands/src/context/mod.rs +++ b/common/commands/src/context/mod.rs @@ -6,6 +6,7 @@ use network_defaults::{ var_names::{API_VALIDATOR, MIXNET_CONTRACT_ADDRESS, NYMD_VALIDATOR, VESTING_CONTRACT_ADDRESS}, NymNetworkDetails, }; +use tap::prelude::*; use validator_client::nymd::{self, AccountId, NymdClient, QueryNymdClient, SigningNymdClient}; pub use validator_client::validator_api::Client as ValidatorApiClient; @@ -58,7 +59,7 @@ pub fn create_signing_client( network_details: &NymNetworkDetails, ) -> Result { let client_config = nymd::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; // get mnemonic let mnemonic = match std::env::var("MNEMONIC") { @@ -87,7 +88,7 @@ pub fn create_query_client( network_details: &NymNetworkDetails, ) -> Result { let client_config = nymd::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; let nymd_url = network_details .endpoints @@ -107,7 +108,7 @@ pub fn create_signing_client_with_validator_api( network_details: &NymNetworkDetails, ) -> Result { let client_config = validator_client::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; // get mnemonic let mnemonic = match std::env::var("MNEMONIC") { @@ -129,7 +130,7 @@ pub fn create_query_client_with_validator_api( network_details: &NymNetworkDetails, ) -> Result { let client_config = validator_client::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; match validator_client::client::Client::new_query(client_config) { Ok(client) => Ok(client), diff --git a/common/commands/src/validator/vesting/balance.rs b/common/commands/src/validator/vesting/balance.rs index 45b2f7866d..0dff560b9f 100644 --- a/common/commands/src/validator/vesting/balance.rs +++ b/common/commands/src/validator/vesting/balance.rs @@ -3,11 +3,11 @@ use clap::Parser; use cosmrs::AccountId; -use log::info; +use log::{error, info}; use validator_client::nymd::{Coin, VestingQueryClient}; -use crate::context::SigningClient; +use crate::context::QueryClient; use crate::utils::show_error; use crate::utils::{pretty_coin, pretty_cosmwasm_coin}; @@ -18,8 +18,16 @@ pub struct Args { pub address: Option, } -pub async fn balance(args: Args, client: SigningClient) { - let account_id = args.address.unwrap_or_else(|| client.address().clone()); +pub async fn balance(args: Args, client: QueryClient, address_from_mnemonic: Option) { + if args.address.is_none() && address_from_mnemonic.is_none() { + error!("Please specify an account address or a mnemonic to get the balance for"); + return; + } + + let account_id = args + .address + .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); + let vesting_address = account_id.to_string(); let denom = client.current_chain_details().mix_denom.base.as_str(); diff --git a/common/commands/src/validator/vesting/query_vesting_schedule.rs b/common/commands/src/validator/vesting/query_vesting_schedule.rs index 23118c1bf5..10a4dc41d3 100644 --- a/common/commands/src/validator/vesting/query_vesting_schedule.rs +++ b/common/commands/src/validator/vesting/query_vesting_schedule.rs @@ -4,11 +4,11 @@ use clap::Parser; use cosmrs::AccountId; use cosmwasm_std::Coin as CosmWasmCoin; -use log::info; +use log::{error, info}; use validator_client::nymd::{Coin, VestingQueryClient}; -use crate::context::SigningClient; +use crate::context::QueryClient; use crate::utils::show_error; use crate::utils::{pretty_coin, pretty_cosmwasm_coin}; @@ -19,8 +19,18 @@ pub struct Args { pub address: Option, } -pub async fn query(args: Args, client: SigningClient) { - let account_id = args.address.unwrap_or_else(|| client.address().clone()); +pub async fn query(args: Args, client: QueryClient, address_from_mnemonic: Option) { + if args.address.is_none() && address_from_mnemonic.is_none() { + error!("Please specify an account address or a mnemonic to get the balance for"); + return; + } + + let account_id = args + .address + .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); + + info!("Checking account {} for a vesting schedule...", account_id); + let vesting_address = account_id.to_string(); let denom = client.current_chain_details().mix_denom.base.as_str(); 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/network-defaults/envs/mainnet.env b/common/network-defaults/envs/mainnet.env deleted file mode 100644 index dce1c3c9c0..0000000000 --- a/common/network-defaults/envs/mainnet.env +++ /dev/null @@ -1,20 +0,0 @@ -CONFIGURED=true - -RUST_LOG=info -RUST_BACKTRACE=1 - -BECH32_PREFIX=n -MIX_DENOM=unym -MIX_DENOM_DISPLAY=nym -STAKE_DENOM=unyx -STAKE_DENOM_DISPLAY=nyx -DENOMS_EXPONENT=6 -MIXNET_CONTRACT_ADDRESS=n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g -VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw -BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy -STATISTICS_SERVICE_DOMAIN_ADDRESS="http://127.0.0.1:8090" -NYMD_VALIDATOR="https://rpc.nyx.nodes.guru/" -API_VALIDATOR="https://validator.nymtech.net/api/" diff --git a/common/network-defaults/envs/qa.env b/common/network-defaults/envs/qa.env deleted file mode 100644 index 842fb54c4d..0000000000 --- a/common/network-defaults/envs/qa.env +++ /dev/null @@ -1,20 +0,0 @@ -CONFIGURED=true - -RUST_LOG=info -RUST_BACKTRACE=1 - -BECH32_PREFIX=n -MIX_DENOM=unym -MIX_DENOM_DISPLAY=nym -STAKE_DENOM=unyx -STAKE_DENOM_DISPLAY=nyx -DENOMS_EXPONENT=6 -MIXNET_CONTRACT_ADDRESS=n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep -VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav -BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw -MULTISIG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs -REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq -STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" -NYMD_VALIDATOR="https://qa-validator.nymtech.net" -API_VALIDATOR="https://qa-validator-api.nymtech.net/api" diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index c2e16dee00..59242376f9 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -85,6 +85,10 @@ pub fn export_to_env() { var_names::MULTISIG_CONTRACT_ADDRESS, MULTISIG_CONTRACT_ADDRESS, ); + set_var_to_default( + var_names::COCONUT_DKG_CONTRACT_ADDRESS, + COCONUT_DKG_CONTRACT_ADDRESS, + ); set_var_to_default( var_names::REWARDING_VALIDATOR_ADDRESS, REWARDING_VALIDATOR_ADDRESS, @@ -125,6 +129,10 @@ pub fn export_to_env_if_not_set() { var_names::MULTISIG_CONTRACT_ADDRESS, MULTISIG_CONTRACT_ADDRESS, ); + set_var_conditionally_to_default( + var_names::COCONUT_DKG_CONTRACT_ADDRESS, + COCONUT_DKG_CONTRACT_ADDRESS, + ); set_var_conditionally_to_default( var_names::REWARDING_VALIDATOR_ADDRESS, REWARDING_VALIDATOR_ADDRESS, 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..ee83423f5f 100644 --- a/common/task/Cargo.toml +++ b/common/task/Cargo.toml @@ -6,8 +6,20 @@ 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"] + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen-futures] +version = "0.4" + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen] +version = "0.2.83" [dev-dependencies] tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal", "test-util", "macros"] } diff --git a/common/task/src/lib.rs b/common/task/src/lib.rs index b8ddb7076a..667936a830 100644 --- a/common/task/src/lib.rs +++ b/common/task/src/lib.rs @@ -2,7 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 pub mod shutdown; +#[cfg(not(target_arch = "wasm32"))] pub mod signal; +pub mod spawn; pub use shutdown::{ShutdownListener, ShutdownNotifier}; -pub use signal::wait_for_signal; +#[cfg(not(target_arch = "wasm32"))] +pub use signal::{wait_for_signal, wait_for_signal_and_error}; + +pub use spawn::spawn_with_report_error; diff --git a/common/task/src/shutdown.rs b/common/task/src/shutdown.rs index 21ea12569d..0f88135f9c 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::{future::pending, 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,23 +136,71 @@ 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, + + // The current operating mode + mode: ShutdownListenerMode, } 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, + mode: ShutdownListenerMode::Listening, } } + // Create a dummy that will never report that we should shutdown. + pub fn dummy() -> ShutdownListener { + let (_notify_tx, notify_rx) = watch::channel(()); + let (task_halt_tx, _task_halt_rx) = mpsc::unbounded_channel(); + let (task_drop_tx, _task_drop_rx) = mpsc::unbounded_channel(); + ShutdownListener { + shutdown: false, + notify: notify_rx, + return_error: task_halt_tx, + drop_error: task_drop_tx, + mode: ShutdownListenerMode::Dummy, + } + } + + pub fn is_dummy(&self) -> bool { + self.mode.is_dummy() + } + pub fn is_shutdown(&self) -> bool { - self.shutdown + if self.mode.is_dummy() { + false + } else { + self.shutdown + } } pub async fn recv(&mut self) { + if self.mode.is_dummy() { + return pending().await; + } if self.shutdown { return; } @@ -93,7 +208,20 @@ impl ShutdownListener { self.shutdown = true; } + pub async fn recv_timeout(&mut self) { + if self.mode.is_dummy() { + return pending().await; + } + #[cfg(not(target_arch = "wasm32"))] + tokio::time::timeout(Self::SHUTDOWN_TIMEOUT, self.recv()) + .await + .expect("Task stopped without shutdown called"); + } + pub fn is_shutdown_poll(&mut self) -> bool { + if self.mode.is_dummy() { + return false; + } if self.shutdown { return true; } @@ -111,6 +239,69 @@ impl ShutdownListener { } } } + + // This listener should to *not* notify the ShutdownNotifier to shutdown when dropped. For + // example when we clone the listener for a task handling connections, we often want to drop + // without signal failure. + pub fn mark_as_success(&mut self) { + self.mode.set_should_not_signal_on_drop(); + } + + pub fn send_we_stopped(&mut self, err: SentError) { + if self.mode.is_dummy() { + return; + } + log::trace!("Notifying we stopped: {:?}", err); + if self.return_error.send(err).is_err() { + log::error!("Failed to send back error message"); + } + } +} + +impl Drop for ShutdownListener { + fn drop(&mut self) { + if !self.mode.should_signal_on_drop() { + return; + } + if !self.is_shutdown_poll() { + log::trace!("Notifying stop on unexpected drop"); + // If we can't send, well then there is not much to do + self.drop_error + .send(Box::new(TaskError::UnexpectedHalt)) + .ok(); + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum ShutdownListenerMode { + // Normal operations + Listening, + // Normal operations, but we don't report back if the we stop by getting dropped. + ListeningButDontReportHalt, + // Dummy mode, for when we don't do anything at all. + Dummy, +} + +impl ShutdownListenerMode { + fn is_dummy(&self) -> bool { + self == &ShutdownListenerMode::Dummy + } + + fn should_signal_on_drop(&self) -> bool { + match self { + ShutdownListenerMode::Listening => true, + ShutdownListenerMode::ListeningButDontReportHalt | ShutdownListenerMode::Dummy => false, + } + } + + fn set_should_not_signal_on_drop(&mut self) { + use ShutdownListenerMode::{Dummy, Listening, ListeningButDontReportHalt}; + *self = match &self { + ListeningButDontReportHalt | Listening => ListeningButDontReportHalt, + Dummy => Dummy, + }; + } } #[cfg(test)] 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/task/src/spawn.rs b/common/task/src/spawn.rs new file mode 100644 index 0000000000..1286abf6dc --- /dev/null +++ b/common/task/src/spawn.rs @@ -0,0 +1,33 @@ +use crate::ShutdownListener; +use std::future::Future; + +#[cfg(target_arch = "wasm32")] +pub(crate) fn spawn(future: F) +where + F: Future + 'static, +{ + wasm_bindgen_futures::spawn_local(future); +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn spawn(future: F) +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + tokio::spawn(future); +} + +pub fn spawn_with_report_error(future: F, mut shutdown: ShutdownListener) +where + F: Future> + Send + 'static, + T: 'static, + E: std::error::Error + Send + 'static, +{ + let future_that_sends = async move { + if let Err(err) = future.await { + shutdown.send_we_stopped(Box::new(err)); + } + }; + spawn(future_that_sends); +} diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 4178ad9a32..8d3e0d5fe8 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -217,18 +217,14 @@ impl NymTopology { #[must_use] pub fn filter_system_version(&self, expected_version: &str) -> Self { - self.filter_node_versions(expected_version, expected_version) + self.filter_node_versions(expected_version) } #[must_use] - pub fn filter_node_versions( - &self, - expected_mix_version: &str, - expected_gateway_version: &str, - ) -> Self { + pub fn filter_node_versions(&self, expected_mix_version: &str) -> Self { NymTopology { mixes: self.mixes.filter_by_version(expected_mix_version), - gateways: self.gateways.filter_by_version(expected_gateway_version), + gateways: self.gateways.clone(), } } } 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/envs/mainnet.env b/envs/mainnet.env index d53b7f4b8b..751081adb8 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -9,7 +9,7 @@ MIX_DENOM_DISPLAY=nym STAKE_DENOM=unyx STAKE_DENOM_DISPLAY=nyx DENOMS_EXPONENT=6 -MIXNET_CONTRACT_ADDRESS=n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g +MIXNET_CONTRACT_ADDRESS=n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 @@ -17,5 +17,5 @@ MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 COCONUT_DKG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" -NYMD_VALIDATOR="https://rpc.nyx.nodes.guru/" +NYMD_VALIDATOR="https://rpc.nymtech.net"; API_VALIDATOR="https://validator.nymtech.net/api/" diff --git a/explorer-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/gateway/gateway-requests/src/lib.rs b/gateway/gateway-requests/src/lib.rs index 98de45c377..d1c1861a8a 100644 --- a/gateway/gateway-requests/src/lib.rs +++ b/gateway/gateway-requests/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2020 - Nym Technologies SA +// Copyright 2020-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 pub use crypto::generic_array; @@ -12,6 +12,10 @@ pub mod iv; pub mod registration; pub mod types; +/// Defines the current version of the communication protocol between gateway and clients. +/// It has to be incremented for any breaking change. +pub const PROTOCOL_VERSION: u8 = 1; + pub type GatewayMac = HmacOutput; // TODO: could using `Mac` trait here for OutputSize backfire? diff --git a/gateway/gateway-requests/src/registration/handshake/state.rs b/gateway/gateway-requests/src/registration/handshake/state.rs index 10c0ae9729..1560b5fd9f 100644 --- a/gateway/gateway-requests/src/registration/handshake/state.rs +++ b/gateway/gateway-requests/src/registration/handshake/state.rs @@ -210,7 +210,10 @@ impl<'a, S> State<'a, S> { match msg { WsMessage::Text(ws_msg) => match types::RegistrationHandshake::try_from(ws_msg) { Ok(reg_handshake_msg) => return match reg_handshake_msg { - types::RegistrationHandshake::HandshakePayload { data } => Ok(data), + // hehe, that's a bit disgusting that the type system requires we explicitly ignore the + // protocol_version field that we actually never attach at this point + // yet another reason for the overdue refactor + types::RegistrationHandshake::HandshakePayload { data, .. } => Ok(data), types::RegistrationHandshake::HandshakeError { message } => Err(HandshakeError::RemoteError(message)), }, Err(_) => error!("Received a non-handshake message during the registration handshake! It's getting dropped."), diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index bfa20a9273..f73f85bca5 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -4,7 +4,7 @@ use crate::authentication::encrypted_address::EncryptedAddressBytes; use crate::iv::IV; use crate::registration::handshake::SharedKeys; -use crate::GatewayMacSize; +use crate::{GatewayMacSize, PROTOCOL_VERSION}; use crypto::generic_array::typenum::Unsigned; use crypto::hmac::recompute_keyed_hmac_and_verify_tag; use crypto::symmetric::stream_cipher; @@ -28,13 +28,22 @@ use credentials::token::bandwidth::TokenCredential; #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "camelCase")] pub enum RegistrationHandshake { - HandshakePayload { data: Vec }, - HandshakeError { message: String }, + HandshakePayload { + #[serde(default)] + protocol_version: Option, + data: Vec, + }, + HandshakeError { + message: String, + }, } impl RegistrationHandshake { pub fn new_payload(data: Vec) -> Self { - RegistrationHandshake::HandshakePayload { data } + RegistrationHandshake::HandshakePayload { + protocol_version: Some(PROTOCOL_VERSION), + data, + } } pub fn new_error>(message: S) -> Self { @@ -115,12 +124,16 @@ pub enum ClientControlRequest { // TODO: should this also contain a MAC considering that at this point we already // have the shared key derived? Authenticate { + #[serde(default)] + protocol_version: Option, address: String, enc_address: String, iv: String, }, #[serde(alias = "handshakePayload")] RegisterHandshakeInitRequest { + #[serde(default)] + protocol_version: Option, data: Vec, }, BandwidthCredential { @@ -137,6 +150,7 @@ impl ClientControlRequest { iv: IV, ) -> Self { ClientControlRequest::Authenticate { + protocol_version: Some(PROTOCOL_VERSION), address: address.as_base58_string(), enc_address: enc_address.to_base58_string(), iv: iv.to_base58_string(), @@ -223,10 +237,14 @@ impl TryInto for ClientControlRequest { #[serde(tag = "type", rename_all = "camelCase")] pub enum ServerResponse { Authenticate { + #[serde(default)] + protocol_version: Option, status: bool, bandwidth_remaining: i64, }, Register { + #[serde(default)] + protocol_version: Option, status: bool, }, Bandwidth { @@ -381,14 +399,37 @@ mod tests { #[test] fn handshake_payload_can_be_deserialized_into_register_handshake_init_request() { let handshake_data = vec![1, 2, 3, 4, 5, 6]; - let handshake_payload = RegistrationHandshake::HandshakePayload { + let handshake_payload_with_protocol = RegistrationHandshake::HandshakePayload { + protocol_version: Some(42), data: handshake_data.clone(), }; - let serialized = serde_json::to_string(&handshake_payload).unwrap(); + let serialized = serde_json::to_string(&handshake_payload_with_protocol).unwrap(); let deserialized = ClientControlRequest::try_from(serialized).unwrap(); match deserialized { - ClientControlRequest::RegisterHandshakeInitRequest { data } => { + ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version, + data, + } => { + assert_eq!(protocol_version, Some(42)); + assert_eq!(data, handshake_data) + } + _ => unreachable!("this branch shouldn't have been reached!"), + } + + let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload { + protocol_version: None, + data: handshake_data.clone(), + }; + let serialized = serde_json::to_string(&handshake_payload_without_protocol).unwrap(); + let deserialized = ClientControlRequest::try_from(serialized).unwrap(); + + match deserialized { + ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version, + data, + } => { + assert!(protocol_version.is_none()); assert_eq!(data, handshake_data) } _ => unreachable!("this branch shouldn't have been reached!"), diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index abe2f30a87..0e4c8dd978 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -18,7 +18,7 @@ use gateway_requests::iv::{IVConversionError, IV}; use gateway_requests::registration::handshake::error::HandshakeError; use gateway_requests::registration::handshake::{gateway_handshake, SharedKeys}; use gateway_requests::types::{ClientControlRequest, ServerResponse}; -use gateway_requests::BinaryResponse; +use gateway_requests::{BinaryResponse, PROTOCOL_VERSION}; use log::*; use mixnet_client::forwarder::MixForwardingSender; use nymsphinx::DestinationAddressBytes; @@ -55,6 +55,9 @@ enum InitialAuthenticationError { #[error("Experienced connection error - {0}")] ConnectionError(#[from] WsError), + + #[error("Attempted to negotiate connection with client using incompatible protocol version. Ours is {current} and the client reports {client:?}")] + IncompatibleProtocol { client: Option, current: u8 }, } impl InitialAuthenticationError { @@ -279,10 +282,7 @@ where // push them to the client if let Err(err) = self.push_packets_to_client(shared_keys, messages).await { - warn!( - "We failed to send stored messages to fresh client - {}", - err - ); + warn!("We failed to send stored messages to fresh client - {err}",); return Err(InitialAuthenticationError::ConnectionError(err)); } else { // if it was successful - remove them from the store @@ -342,6 +342,27 @@ where } } + fn check_client_protocol( + &self, + client_protocol: Option, + ) -> Result<(), InitialAuthenticationError> { + // right now there are no failure cases here, but this might change in the future + match client_protocol { + Some(v) if v == PROTOCOL_VERSION => { + info!("the client is using exactly the same protocol version as we are. We're good to continue!"); + Ok(()) + } + v => { + let err = InitialAuthenticationError::IncompatibleProtocol { + client: v, + current: PROTOCOL_VERSION, + }; + error!("{err}"); + Err(err) + } + } + } + /// Using the received challenge data, i.e. client's address as well the ciphertext of it plus /// a fresh IV, attempts to authenticate the client by checking whether the ciphertext matches /// the expected value if encrypted with the shared key. @@ -389,6 +410,7 @@ where /// * `iv`: fresh IV received with the request. async fn handle_authenticate( &mut self, + client_protocol_version: Option, address: String, enc_address: String, iv: String, @@ -396,6 +418,8 @@ where where S: AsyncRead + AsyncWrite + Unpin, { + self.check_client_protocol(client_protocol_version)?; + let address = DestinationAddressBytes::try_from_base58_string(address) .map_err(|err| InitialAuthenticationError::MalformedClientAddress(err.to_string()))?; let encrypted_address = EncryptedAddressBytes::try_from_base58_string(enc_address)?; @@ -420,6 +444,7 @@ where Ok(InitialAuthResult::new( client_details, ServerResponse::Authenticate { + protocol_version: Some(PROTOCOL_VERSION), status, bandwidth_remaining, }, @@ -474,11 +499,14 @@ where /// * `init_data`: init payload of the registration handshake. async fn handle_register( &mut self, + client_protocol_version: Option, init_data: Vec, ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, { + self.check_client_protocol(client_protocol_version)?; + let remote_identity = Self::extract_remote_identity_from_register_init(&init_data)?; let remote_address = remote_identity.derive_destination_address(); @@ -493,7 +521,10 @@ where Ok(InitialAuthResult::new( Some(client_details), - ServerResponse::Register { status }, + ServerResponse::Register { + protocol_version: Some(PROTOCOL_VERSION), + status, + }, )) } @@ -513,13 +544,18 @@ where if let Ok(request) = ClientControlRequest::try_from(raw_request) { match request { ClientControlRequest::Authenticate { + protocol_version, address, enc_address, iv, - } => self.handle_authenticate(address, enc_address, iv).await, - ClientControlRequest::RegisterHandshakeInitRequest { data } => { - self.handle_register(data).await + } => { + self.handle_authenticate(protocol_version, address, enc_address, iv) + .await } + ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version, + data, + } => self.handle_register(protocol_version, data).await, // won't accept anything else (like bandwidth) without prior authentication _ => Err(InitialAuthenticationError::InvalidRequest), } 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 b18518d32e..c7d953327d 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -639,7 +639,7 @@ dependencies = [ [[package]] name = "client-core" -version = "1.1.0" +version = "1.1.1" dependencies = [ "async-trait", "client-connections", @@ -664,6 +664,7 @@ dependencies = [ "thiserror", "time 0.3.14", "tokio", + "tokio-stream", "topology", "url", "validator-client", @@ -3385,7 +3386,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.0" +version = "1.1.1" dependencies = [ "clap", "client-connections", @@ -5609,8 +5610,12 @@ dependencies = [ name = "task" version = "0.1.0" dependencies = [ + "futures", "log", + "thiserror", "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", ] [[package]] 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/error.rs b/nym-connect/src-tauri/src/error.rs index b2b44ee345..cb677aac68 100644 --- a/nym-connect/src-tauri/src/error.rs +++ b/nym-connect/src-tauri/src/error.rs @@ -1,3 +1,4 @@ +use client_core::client::replies::reply_storage::fs_backend; use client_core::error::ClientCoreError; use serde::{Serialize, Serializer}; use thiserror::Error; @@ -33,7 +34,7 @@ pub enum BackendError { #[error("{source}")] ClientCoreError { #[from] - source: ClientCoreError, + source: ClientCoreError, }, #[error("Could not send disconnect signal to the SOCKS5 client")] diff --git a/nym-connect/src-tauri/src/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/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index 5d63555ddb..1859cf0320 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -114,7 +114,6 @@ export const BondedMixnode = ({ ), id: 'actions-cell', diff --git a/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx b/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx index 88518abc28..e2145e431e 100644 --- a/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Box, FormHelperText, Stack, TextField } from '@mui/material'; +import { Box, Stack } from '@mui/material'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; @@ -7,66 +7,74 @@ import { DecCoin } from '@nymproject/types'; import { TokenPoolSelector, TPoolOption } from 'src/components/TokenPoolSelector'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { useGetFee } from 'src/hooks/useGetFee'; -import { validateAmount, validateKey } from 'src/utils'; +import { validateAmount } from 'src/utils'; +import { simulateBondMore, simulateVestingBondMore } from 'src/requests'; +import { TBondMoreArgs } from 'src/types'; +import { TBondedMixnode } from 'src/context'; export const BondMoreModal = ({ - currentBond, + node, userBalance, - hasVestingTokens, - onConfirm, + onBondMore, onClose, + onError, }: { - currentBond: DecCoin; + node: TBondedMixnode; userBalance?: string; - hasVestingTokens: boolean; - onConfirm: (args: { additionalBond: DecCoin; signature: string; tokenPool: TPoolOption }) => Promise; + onBondMore: (data: TBondMoreArgs, tokenPool: TPoolOption) => Promise; onClose: () => void; + onError: (e: string) => void; }) => { - const { fee, resetFeeState } = useGetFee(); + const { bond: currentBond, proxy } = node; + const { fee, getFee, resetFeeState, feeError } = useGetFee(); const [additionalBond, setAdditionalBond] = useState({ amount: '0', denom: currentBond.denom }); - const [signature, setSignature] = useState(''); - const [tokenPool, setTokenPool] = useState('balance'); const [errorAmount, setErrorAmount] = useState(false); - const [errorSignature, setErrorSignature] = useState(false); - const handleOnOk = async () => { - const errors = { - amount: false, - signature: false, - }; - - if (!validateKey(signature || '', 64)) { - errors.signature = true; + useEffect(() => { + if (feeError) { + onError(feeError); } + }, [feeError]); - if (!additionalBond?.amount) { - errors.amount = true; - } + const handleConfirm = async () => { + const data = { additionalPledge: additionalBond }; + const tokenPool = proxy ? 'locked' : 'balance'; + await onBondMore(data, tokenPool); + }; - if (additionalBond && !(await validateAmount(additionalBond.amount, '1'))) { - errors.amount = true; - } + const handleAmountChanged = async (value: DecCoin) => { + setAdditionalBond(value); + const { amount } = value; - if (!errors.amount && !errors.signature) { - onConfirm({ additionalBond, signature, tokenPool }); + if (!amount) { + setErrorAmount(true); } else { - setErrorAmount(errors.amount); - setErrorSignature(errors.signature); + const validAmount = await validateAmount(amount, '1'); + if (!validAmount) { + setErrorAmount(true); + return; + } + setErrorAmount(false); } }; - useEffect(() => { - setErrorAmount(false); - }, [additionalBond]); + const handleOnOk = async () => { + if (!proxy) { + await getFee(simulateBondMore, { additionalPledge: additionalBond }); + } else { + await getFee(simulateVestingBondMore, { additionalPledge: additionalBond }); + } + }; if (fee) return ( onConfirm({ additionalBond, signature, tokenPool })} + onClose={onClose} onPrev={resetFeeState} + onConfirm={handleConfirm} > @@ -80,36 +88,23 @@ export const BondMoreModal = ({ subHeader="Bond more tokens on your node and receive more rewards" okLabel="Next" onOk={handleOnOk} - okDisabled={errorAmount || errorSignature} + okDisabled={errorAmount} onClose={onClose} > - {hasVestingTokens && setTokenPool(pool)} />} { - setAdditionalBond(value); - setErrorSignature(false); + handleAmountChanged(value); }} fullWidth validationError={errorAmount ? 'Please enter a valid amount' : undefined} /> - - setSignature(e.target.value)} - InputLabelProps={{ shrink: true }} - /> - {errorSignature && Invalid signature} - - diff --git a/nym-wallet/src/components/Bonding/modals/BondOversaturatedModal.tsx b/nym-wallet/src/components/Bonding/modals/BondOversaturatedModal.tsx new file mode 100644 index 0000000000..be3ea33869 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/BondOversaturatedModal.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Stack, Typography } from '@mui/material'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; + +export const BondOversaturatedModal: React.FC<{ + open: boolean; + saturationPercentage: string; + onClose?: () => void; + onContinue?: () => void; +}> = ({ open, saturationPercentage, onClose, onContinue }) => ( + onContinue?.()} + header="Bond More" + okLabel="Bond More" + buttonFullWidth + > + + t.palette.nym.red }} + >{`Your node saturation: ${saturationPercentage}%`} + Your node is over saturated, are you sure you want to bond more? + + +); diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index 524a9ea0be..7225360456 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -8,7 +8,7 @@ import { } from '@nymproject/types'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import Big from 'big.js'; -import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs } from 'src/types'; +import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs, TBondMoreArgs } from 'src/types'; import { Console } from 'src/utils/console'; import { bondGateway as bondGatewayRequest, @@ -18,6 +18,8 @@ import { getMixnodeBondDetails, unbondGateway as unbondGatewayRequest, unbondMixNode as unbondMixnodeRequest, + bondMore as bondMoreRequest, + vestingBondMore, vestingBondGateway, vestingBondMixNode, vestingUnbondGateway, @@ -100,7 +102,7 @@ export type TBondingContext = { bondMixnode: (data: TBondMixNodeArgs, tokenPool: TokenPool) => Promise; bondGateway: (data: TBondGatewayArgs, tokenPool: TokenPool) => Promise; unbond: (fee?: FeeDetails) => Promise; - bondMore: (signature: string, amount: DecCoin, fee?: FeeDetails) => Promise; + bondMore: (data: TBondMoreArgs, tokenPool: TokenPool) => Promise; redeemRewards: (fee?: FeeDetails) => Promise; updateMixnode: (pm: string, fee?: FeeDetails) => Promise; checkOwnership: () => Promise; @@ -257,7 +259,6 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod rewarding_details, bond_information: { mix_id }, } = data; - const { status, stakeSaturation, estimatedRewards } = await getAdditionalMixnodeDetails(mix_id); const setProbabilities = await getSetProbabilities(mix_id); const nodeDescription = await getNodeDescription( @@ -431,9 +432,28 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod return tx; }; - const bondMore = async (_signature: string, _additionalBond: DecCoin) => - // TODO to implement - undefined; + const bondMore = async (data: TBondMoreArgs, tokenPool: TokenPool) => { + let tx: TransactionExecuteResult | undefined; + setIsLoading(true); + try { + if (tokenPool === 'balance') { + tx = await bondMoreRequest(data); + await userBalance.fetchBalance(); + } + if (tokenPool === 'locked') { + tx = await vestingBondMore(data); + await userBalance.fetchTokenAllocation(); + } + + return tx; + } catch (e: any) { + Console.warn(e); + setError(`an error occurred: ${e}`); + } finally { + setIsLoading(false); + } + return undefined; + }; const memoizedValue = useMemo( () => ({ diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 8d97deb090..95e770850b 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -152,7 +152,7 @@ export const MockBondingContextProvider = ({ return TxResultMock; }; - const bondMore = async (_signature: string, _additionalBond: DecCoin) => { + const bondMore = async (): Promise => { setIsLoading(true); await mockSleep(SLEEP_MS); triggerStateUpdate(); diff --git a/nym-wallet/src/pages/bonding/Bonding.tsx b/nym-wallet/src/pages/bonding/Bonding.tsx index d1ec197b0f..1aa6bd5ef2 100644 --- a/nym-wallet/src/pages/bonding/Bonding.tsx +++ b/nym-wallet/src/pages/bonding/Bonding.tsx @@ -1,6 +1,6 @@ import React, { useContext, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { FeeDetails } from '@nymproject/types'; +import { FeeDetails, decimalToFloatApproximation } from '@nymproject/types'; import { Box } from '@mui/material'; import { TPoolOption } from 'src/components'; import { Bond } from 'src/components/Bonding/Bond'; @@ -8,28 +8,35 @@ import { BondedMixnode } from 'src/components/Bonding/BondedMixnode'; import { TBondedMixnodeActions } from 'src/components/Bonding/BondedMixnodeActions'; import { BondGatewayModal } from 'src/components/Bonding/modals/BondGatewayModal'; import { BondMixnodeModal } from 'src/components/Bonding/modals/BondMixnodeModal'; +import { BondMoreModal } from 'src/components/Bonding/modals/BondMoreModal'; +import { BondOversaturatedModal } from 'src/components/Bonding/modals/BondOversaturatedModal'; import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal'; import { ErrorModal } from 'src/components/Modals/ErrorModal'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { AppContext, urls } from 'src/context/main'; -import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs } from 'src/types'; +import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs, TBondMoreArgs } from 'src/types'; import { BondedGateway } from 'src/components/Bonding/BondedGateway'; import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal'; import { BondingContextProvider, useBondingContext } from '../../context'; +import { getMixnodeStakeSaturation } from '../../requests'; const Bonding = () => { - const [showModal, setShowModal] = useState<'bond-mixnode' | 'bond-gateway' | 'bond-more' | 'unbond' | 'redeem'>(); + const [showModal, setShowModal] = useState< + 'bond-mixnode' | 'bond-gateway' | 'bond-more' | 'bond-more-oversaturated' | 'unbond' | 'redeem' + >(); const [confirmationDetails, setConfirmationDetails] = useState(); + const [saturationPercentage, setSaturationPercentage] = useState(); const { network, clientDetails, - userBalance: { originalVesting }, + userBalance: { originalVesting, balance }, } = useContext(AppContext); const navigate = useNavigate(); - const { bondedNode, bondMixnode, bondGateway, redeemRewards, isLoading, checkOwnership } = useBondingContext(); + const { bondedNode, bondMixnode, bondGateway, redeemRewards, isLoading, checkOwnership, bondMore } = + useBondingContext(); const handleCloseModal = async () => { setShowModal(undefined); @@ -66,6 +73,16 @@ const Bonding = () => { }); }; + const handleBondMore = async (data: TBondMoreArgs, tokenPool: TPoolOption) => { + setShowModal(undefined); + const tx = await bondMore(data, tokenPool); + setConfirmationDetails({ + status: 'success', + title: 'Bond More successful', + txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, + }); + }; + const handleRedeemReward = async (fee?: FeeDetails) => { setShowModal(undefined); const tx = await redeemRewards(fee); @@ -76,9 +93,33 @@ const Bonding = () => { }); }; - const handleBondedMixnodeAction = (action: TBondedMixnodeActions) => { + const handleCheckStakeSaturation = async (newMixId: number) => { + try { + const newSaturation = decimalToFloatApproximation( + (await getMixnodeStakeSaturation(newMixId)).uncapped_saturation, + ); + if (newSaturation && newSaturation > 1) { + const saturationPercentage = Math.round(newSaturation * 100); + return { isOverSaturated: true, saturationPercentage }; + } + return { isOverSaturated: false, saturationPercentage: undefined }; + } catch (e) { + console.error('Error fetching the saturation, error:', e); + return { isOverSaturated: false, saturationPercentage: undefined }; + } + }; + + const handleBondedMixnodeAction = async (action: TBondedMixnodeActions) => { switch (action) { case 'bondMore': { + if (bondedNode && isMixnode(bondedNode)) { + const { isOverSaturated, saturationPercentage } = await handleCheckStakeSaturation(bondedNode.mixId); + if (isOverSaturated && saturationPercentage) { + setShowModal('bond-more-oversaturated'); + setSaturationPercentage(saturationPercentage.toString()); + break; + } + } setShowModal('bond-more'); break; } @@ -112,6 +153,7 @@ const Bonding = () => { {bondedNode && isGateway(bondedNode) && ( )} + {showModal === 'bond-mixnode' && ( { /> )} + {showModal === 'bond-more-oversaturated' && saturationPercentage && ( + setShowModal(undefined)} + onContinue={() => setShowModal('bond-more')} + saturationPercentage={saturationPercentage} + /> + )} + + {showModal === 'bond-more' && bondedNode && isMixnode(bondedNode) && ( + setShowModal(undefined)} + onError={handleError} + /> + )} + {showModal === 'redeem' && bondedNode && isMixnode(bondedNode) && ( @@ -32,3 +32,6 @@ export const unbond = async (type: EnumNodeType) => { if (type === EnumNodeType.mixnode) return unbondMixNode(); return unbondGateway(); }; + +export const bondMore = async (args: TBondMoreArgs) => + invokeWrapper('pledge_more', args); diff --git a/nym-wallet/src/requests/simulate.ts b/nym-wallet/src/requests/simulate.ts index ddeae6ab2e..4a1b82f0d4 100644 --- a/nym-wallet/src/requests/simulate.ts +++ b/nym-wallet/src/requests/simulate.ts @@ -63,3 +63,9 @@ export const simulateClaimOperatorReward = async () => invokeWrapper export const simulateVestingClaimOperatorReward = async () => invokeWrapper('simulate_vesting_claim_operator_reward'); + +export const simulateBondMore = async (args: any) => + invokeWrapper('simulate_pledge_more', args); + +export const simulateVestingBondMore = async (args: any) => + invokeWrapper('simulate_vesting_pledge_more', args); diff --git a/nym-wallet/src/requests/vesting.ts b/nym-wallet/src/requests/vesting.ts index 907f42c499..bad2f7febf 100644 --- a/nym-wallet/src/requests/vesting.ts +++ b/nym-wallet/src/requests/vesting.ts @@ -105,3 +105,14 @@ export const vestingClaimOperatorReward = async (fee?: Fee) => export const vestingClaimDelegatorRewards = async (mixId: number) => invokeWrapper('vesting_claim_delegator_reward', { mixId }); + +export const vestingBondMore = async ({ + fee, + additionalPledge, +}: { + fee?: Fee; + additionalPledge: DecCoin; +}) => invokeWrapper('vesting_pledge_more', { + fee, + additionalPledge, +}); diff --git a/nym-wallet/src/types/global.ts b/nym-wallet/src/types/global.ts index d5bb61c26d..2c16901cfe 100644 --- a/nym-wallet/src/types/global.ts +++ b/nym-wallet/src/types/global.ts @@ -49,6 +49,12 @@ export type TBondMixNodeArgs = { fee?: Fee; }; +export type TBondMoreArgs = { + additionalPledge: DecCoin; + fee?: Fee; + +}; + export type TNodeDescription = { name: string; description: string; 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 965e2e6b16..2c20efedaa 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -203,9 +203,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 2ec5af6404..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" @@ -18,6 +18,7 @@ serde_json = "1" tokio = { version = "1.11", features = [ "net", "rt-multi-thread", "macros", "signal"] } bip39 = "1.0.1" anyhow = "1" +tap = "1" nym-cli-commands = { path = "../../common/commands" } logging = { path = "../../common/logging"} diff --git a/tools/nym-cli/src/validator/vesting.rs b/tools/nym-cli/src/validator/vesting.rs index d87ca2c9ff..202011bc3c 100644 --- a/tools/nym-cli/src/validator/vesting.rs +++ b/tools/nym-cli/src/validator/vesting.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use network_defaults::NymNetworkDetails; -use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::context::{create_query_client, create_signing_client, ClientArgs}; pub(crate) async fn execute( global_args: ClientArgs, @@ -19,18 +19,22 @@ pub(crate) async fn execute( .await } Some(nym_cli_commands::validator::vesting::VestingScheduleCommands::Query(args)) => { + let address_from_args = args.address.clone(); nym_cli_commands::validator::vesting::query_vesting_schedule::query( args, - create_signing_client(global_args, network_details)?, + create_query_client(network_details)?, + address_from_args, ) .await } Some(nym_cli_commands::validator::vesting::VestingScheduleCommands::VestedBalance( args, )) => { + let address_from_args = args.address.clone(); nym_cli_commands::validator::vesting::balance::balance( args, - create_signing_client(global_args, network_details)?, + create_query_client(network_details)?, + address_from_args, ) .await } 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 ", diff --git a/validator-api/src/contract_cache/mod.rs b/validator-api/src/contract_cache/mod.rs index 95d984533c..b4538ea36b 100644 --- a/validator-api/src/contract_cache/mod.rs +++ b/validator-api/src/contract_cache/mod.rs @@ -2,13 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nymd_client::Client; -use crate::storage::ValidatorApiStorage; use ::time::OffsetDateTime; use anyhow::Result; -use mixnet_contract_common::mixnode::MixNodeDetails; -use mixnet_contract_common::reward_params::{Performance, RewardingParams}; use mixnet_contract_common::{ - GatewayBond, IdentityKey, Interval, MixId, MixNodeBond, RewardedSetNodeStatus, + mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, IdentityKey, Interval, + MixId, MixNodeBond, RewardedSetNodeStatus, }; use okapi::openapi3::OpenApi; use rocket::fairing::AdHoc; @@ -16,18 +14,17 @@ use rocket::Route; use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; use serde::Serialize; -use std::collections::HashMap; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; +use std::ops::Deref; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use task::ShutdownListener; use tokio::sync::{watch, RwLock}; use tokio::time; -use validator_api_requests::models::{MixNodeBondAnnotated, MixnodeStatus}; +use validator_api_requests::models::MixnodeStatus; use validator_client::nymd::CosmWasmClient; -pub(crate) mod reward_estimate; pub(crate) mod routes; // The cache can emit notifications to listeners about the current state @@ -42,9 +39,6 @@ pub struct ValidatorCacheRefresher { cache: ValidatorCache, caching_interval: Duration, - // Readonly: some of the quantities cached depends on values from the storage. - storage: Option, - // Notify listeners that the cache has been updated update_notifier: watch::Sender, } @@ -56,14 +50,14 @@ pub struct ValidatorCache { } struct ValidatorCacheInner { - mixnodes: Cache>, + mixnodes: Cache>, gateways: Cache>, mixnodes_blacklist: Cache>, gateways_blacklist: Cache>, - rewarded_set: Cache>, - active_set: Cache>, + rewarded_set: Cache>, + active_set: Cache>, current_reward_params: Cache>, current_interval: Cache>, @@ -102,90 +96,33 @@ impl Cache { } } +impl Deref for Cache { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + impl ValidatorCacheRefresher { pub(crate) fn new( nymd_client: Client, caching_interval: Duration, cache: ValidatorCache, - storage: Option, ) -> Self { let (tx, _) = watch::channel(CacheNotification::Start); ValidatorCacheRefresher { nymd_client, cache, caching_interval, - storage, update_notifier: tx, } } - async fn get_performance(&self, mix_id: MixId, epoch: Interval) -> Option { - self.storage - .as_ref()? - .get_average_mixnode_uptime_in_the_last_24hrs( - mix_id, - epoch.current_epoch_end_unix_timestamp(), - ) - .await - .ok() - .map(Into::into) - } - pub fn subscribe(&self) -> watch::Receiver { self.update_notifier.subscribe() } - async fn annotate_node_with_details( - &self, - mixnodes: Vec, - interval_reward_params: RewardingParams, - current_interval: Interval, - rewarded_set: &HashMap, - ) -> Vec { - let mut annotated = Vec::new(); - for mixnode in mixnodes { - let stake_saturation = mixnode - .rewarding_details - .bond_saturation(&interval_reward_params); - - let uncapped_stake_saturation = mixnode - .rewarding_details - .uncapped_bond_saturation(&interval_reward_params); - - let performance = self - .get_performance(mixnode.mix_id(), current_interval) - .await - .unwrap_or_default(); - - let rewarded_set_status = rewarded_set.get(&mixnode.mix_id()).cloned(); - - let reward_estimate = reward_estimate::compute_reward_estimate( - &mixnode, - performance, - rewarded_set_status, - interval_reward_params, - current_interval, - ); - - let (estimated_operator_apy, estimated_delegators_apy) = - reward_estimate::compute_apy_from_reward( - &mixnode, - reward_estimate, - current_interval, - ); - - annotated.push(MixNodeBondAnnotated { - mixnode_details: mixnode, - stake_saturation, - uncapped_stake_saturation, - performance, - estimated_operator_apy, - estimated_delegators_apy, - }); - } - annotated - } - async fn get_rewarded_set_map(&self) -> HashMap where C: CosmWasmClient + Sync + Send, @@ -198,9 +135,9 @@ impl ValidatorCacheRefresher { } fn collect_rewarded_and_active_set_details( - all_mixnodes: &[MixNodeBondAnnotated], + all_mixnodes: &[MixNodeDetails], rewarded_set_nodes: &HashMap, - ) -> (Vec, Vec) { + ) -> (Vec, Vec) { let mut active_set = Vec::new(); let mut rewarded_set = Vec::new(); @@ -226,14 +163,10 @@ impl ValidatorCacheRefresher { let mixnodes = self.nymd_client.get_mixnodes().await?; let gateways = self.nymd_client.get_gateways().await?; - let rewarded_set = self.get_rewarded_set_map().await; - - let mixnodes = self - .annotate_node_with_details(mixnodes, rewarding_params, current_interval, &rewarded_set) - .await; + let rewarded_set_map = self.get_rewarded_set_map().await; let (rewarded_set, active_set) = - Self::collect_rewarded_and_active_set_details(&mixnodes, &rewarded_set); + Self::collect_rewarded_and_active_set_details(&mixnodes, &rewarded_set_map); info!( "Updating validator cache. There are {} mixnodes and {} gateways", @@ -324,10 +257,10 @@ impl ValidatorCache { async fn update_cache( &self, - mixnodes: Vec, + mixnodes: Vec, gateways: Vec, - rewarded_set: Vec, - active_set: Vec, + rewarded_set: Vec, + active_set: Vec, rewarding_params: RewardingParams, current_interval: Interval, ) { @@ -422,7 +355,7 @@ impl ValidatorCache { error!("Failed to update gateways blacklist"); } - pub async fn mixnodes_detailed(&self) -> Vec { + pub async fn mixnodes(&self) -> Vec { let blacklist = self.mixnodes_blacklist().await; let mixnodes = match time::timeout(Duration::from_millis(100), self.inner.read()).await { Ok(cache) => cache.mixnodes.clone(), @@ -444,14 +377,6 @@ impl ValidatorCache { } } - pub async fn mixnodes(&self) -> Vec { - self.mixnodes_detailed() - .await - .into_iter() - .map(|bond| bond.mixnode_details) - .collect() - } - pub async fn mixnodes_basic(&self) -> Vec { match time::timeout(Duration::from_millis(100), self.inner.read()).await { Ok(cache) => cache @@ -459,7 +384,7 @@ impl ValidatorCache { .clone() .into_inner() .into_iter() - .map(|bond| bond.mixnode_details.bond_information) + .map(|bond| bond.bond_information) .collect(), Err(e) => { error!("{}", e); @@ -500,7 +425,7 @@ impl ValidatorCache { } } - pub async fn rewarded_set_detailed(&self) -> Cache> { + pub async fn rewarded_set(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { Ok(cache) => cache.rewarded_set.clone(), Err(e) => { @@ -510,16 +435,7 @@ impl ValidatorCache { } } - pub async fn rewarded_set(&self) -> Vec { - self.rewarded_set_detailed() - .await - .value - .into_iter() - .map(|bond| bond.mixnode_details) - .collect() - } - - pub async fn active_set_detailed(&self) -> Cache> { + pub async fn active_set(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { Ok(cache) => cache.active_set.clone(), Err(e) => { @@ -529,15 +445,6 @@ impl ValidatorCache { } } - pub async fn active_set(&self) -> Vec { - self.active_set_detailed() - .await - .value - .into_iter() - .map(|bond| bond.mixnode_details) - .collect() - } - pub(crate) async fn interval_reward_params(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { Ok(cache) => cache.current_reward_params.clone(), @@ -558,24 +465,21 @@ impl ValidatorCache { } } - pub async fn mixnode_details( - &self, - mix_id: MixId, - ) -> (Option, MixnodeStatus) { + pub async fn mixnode_details(&self, mix_id: MixId) -> (Option, MixnodeStatus) { // it might not be the most optimal to possibly iterate the entire vector to find (or not) // the relevant value. However, the vectors are relatively small (< 10_000 elements, < 1000 for active set) - let active_set = &self.active_set_detailed().await.value; + let active_set = &self.active_set().await.value; if let Some(bond) = active_set.iter().find(|mix| mix.mix_id() == mix_id) { return (Some(bond.clone()), MixnodeStatus::Active); } - let rewarded_set = &self.rewarded_set_detailed().await.value; + let rewarded_set = &self.rewarded_set().await.value; if let Some(bond) = rewarded_set.iter().find(|mix| mix.mix_id() == mix_id) { return (Some(bond.clone()), MixnodeStatus::Standby); } - let all_bonded = &self.mixnodes_detailed().await; + let all_bonded = &self.mixnodes().await; if let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) { (Some(bond.clone()), MixnodeStatus::Inactive) } else { diff --git a/validator-api/src/contract_cache/routes.rs b/validator-api/src/contract_cache/routes.rs index 45d0b290ac..6aba1ba4f9 100644 --- a/validator-api/src/contract_cache/routes.rs +++ b/validator-api/src/contract_cache/routes.rs @@ -1,15 +1,21 @@ // Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::contract_cache::ValidatorCache; -use mixnet_contract_common::mixnode::MixNodeDetails; -use mixnet_contract_common::reward_params::RewardingParams; -use mixnet_contract_common::{GatewayBond, Interval, MixId}; -use rocket::serde::json::Json; -use rocket::State; +use crate::{ + contract_cache::ValidatorCache, + node_status_api::{ + helpers::{_get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed}, + NodeStatusCache, + }, +}; +use mixnet_contract_common::{ + mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, Interval, MixId, +}; +use validator_api_requests::models::MixNodeBondAnnotated; + +use rocket::{serde::json::Json, State}; use rocket_okapi::openapi; use std::collections::HashSet; -use validator_api_requests::models::MixNodeBondAnnotated; #[openapi(tag = "contract-cache")] #[get("/mixnodes")] @@ -17,12 +23,19 @@ pub async fn get_mixnodes(cache: &State) -> Json Redirect { +// Redirect::to(uri!("/v1/status/mixnodes/detailed")) +// } +// ``` #[openapi(tag = "contract-cache")] #[get("/mixnodes/detailed")] pub async fn get_mixnodes_detailed( - cache: &State, + cache: &State, ) -> Json> { - Json(cache.mixnodes_detailed().await) + Json(_get_mixnodes_detailed(cache).await) } #[openapi(tag = "contract-cache")] @@ -34,29 +47,43 @@ pub async fn get_gateways(cache: &State) -> Json) -> Json> { - Json(cache.rewarded_set().await) + Json(cache.rewarded_set().await.value) } +// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, +// replace this with +// ``` +// pub fn get_mixnodes_set_detailed() -> Redirect { +// Redirect::to(uri!("/v1/status/mixnodes/rewarded/detailed")) +// } +// ``` #[openapi(tag = "contract-cache")] #[get("/mixnodes/rewarded/detailed")] pub async fn get_rewarded_set_detailed( - cache: &State, + cache: &State, ) -> Json> { - Json(cache.rewarded_set_detailed().await.value) + Json(_get_rewarded_set_detailed(cache).await) } #[openapi(tag = "contract-cache")] #[get("/mixnodes/active")] pub async fn get_active_set(cache: &State) -> Json> { - Json(cache.active_set().await) + Json(cache.active_set().await.value) } +// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, +// replace this with +// ``` +// pub fn get_active_set_detailed() -> Redirect { +// Redirect::to(uri!("/status/mixnodes/active/detailed")) +// } +// ``` #[openapi(tag = "contract-cache")] #[get("/mixnodes/active/detailed")] pub async fn get_active_set_detailed( - cache: &State, + cache: &State, ) -> Json> { - Json(cache.active_set_detailed().await.value) + Json(_get_active_set_detailed(cache).await) } #[openapi(tag = "contract-cache")] diff --git a/validator-api/src/epoch_operations/mod.rs b/validator-api/src/epoch_operations/mod.rs index cb33962890..06141e7863 100644 --- a/validator-api/src/epoch_operations/mod.rs +++ b/validator-api/src/epoch_operations/mod.rs @@ -156,7 +156,7 @@ impl RewardedSetUpdater { Err(err) => { warn!("failed to obtain the current rewarded set - {}. falling back to the cached version", err); self.validator_cache - .rewarded_set_detailed() + .rewarded_set() .await .into_inner() .into_iter() diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index 5116896858..3055d57f88 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -121,6 +121,7 @@ fn parse_args() -> ArgMatches { Arg::with_name(CONFIG_ENV_FILE) .help("Path pointing to an env file that configures the validator API") .long(CONFIG_ENV_FILE) + .short('c') .takes_value(true) ) .arg( @@ -577,7 +578,6 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { signing_nymd_client.clone(), config.get_caching_interval(), validator_cache.clone(), - Some(storage.clone()), ); let validator_cache_listener = validator_cache_refresher.subscribe(); let shutdown_listener = shutdown.subscribe(); @@ -599,7 +599,6 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { nymd_client, config.get_caching_interval(), validator_cache.clone(), - None, ); let validator_cache_listener = validator_cache_refresher.subscribe(); let shutdown_listener = shutdown.subscribe(); @@ -611,11 +610,13 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { // Spawn the node status cache refresher. // It is primarily refreshed in-sync with the validator cache, however provide a fallback // caching interval that is twice the validator cache + let storage = rocket.state::().cloned(); let mut validator_api_cache_refresher = node_status_api::NodeStatusCacheRefresher::new( node_status_cache, + config.get_caching_interval().saturating_mul(2), validator_cache, validator_cache_listener, - config.get_caching_interval().saturating_mul(2), + storage, ); let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { validator_api_cache_refresher.run(shutdown_listener).await }); diff --git a/validator-api/src/network_monitor/monitor/sender.rs b/validator-api/src/network_monitor/monitor/sender.rs index 4ec0e9ce45..d2fa815a11 100644 --- a/validator-api/src/network_monitor/monitor/sender.rs +++ b/validator-api/src/network_monitor/monitor/sender.rs @@ -209,7 +209,7 @@ impl PacketSender { ack_sender, fresh_gateway_client_data.gateway_response_timeout, Some(fresh_gateway_client_data.bandwidth_controller.clone()), - None, + task::ShutdownListener::dummy(), ); gateway_client diff --git a/validator-api/src/node_status_api/cache.rs b/validator-api/src/node_status_api/cache.rs index ef5df963bf..dc8dc7447f 100644 --- a/validator-api/src/node_status_api/cache.rs +++ b/validator-api/src/node_status_api/cache.rs @@ -2,25 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::contract_cache::{Cache, CacheNotification, ValidatorCache}; -use contracts_common::truncate_decimal; -use mixnet_contract_common::{MixId, MixNodeDetails, RewardingParams}; +use crate::storage::ValidatorApiStorage; +use mixnet_contract_common::reward_params::Performance; +use mixnet_contract_common::{ + Interval, MixId, MixNodeDetails, RewardedSetNodeStatus, RewardingParams, +}; use rocket::fairing::AdHoc; -use serde::Serialize; +use std::collections::HashMap; use std::{sync::Arc, time::Duration}; -use tap::TapFallible; use task::ShutdownListener; +use tokio::sync::RwLockReadGuard; use tokio::{ sync::{watch, RwLock}, time, }; -use validator_api_requests::models::InclusionProbability; +use validator_api_requests::models::{MixNodeBondAnnotated, MixnodeStatus}; + +use self::inclusion_probabilities::InclusionProbabilities; + +use super::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; + +mod inclusion_probabilities; const CACHE_TIMOUT_MS: u64 = 100; -const MAX_SIMULATION_SAMPLES: u64 = 5000; -const MAX_SIMULATION_TIME_SEC: u64 = 15; enum NodeStatusCacheError { SimulationFailed, + SourceDataMissing, } // A node status cache suitable for caching values computed in one sweep, such as active set @@ -32,27 +40,16 @@ pub struct NodeStatusCache { inner: Arc>, } +#[derive(Default)] struct NodeStatusCacheInner { + mixnodes_annotated: Cache>, + rewarded_set_annotated: Cache>, + active_set_annotated: Cache>, + + // Estimated active set inclusion probabilities from Monte Carlo simulation inclusion_probabilities: Cache, } -#[derive(Clone, Default, Serialize, schemars::JsonSchema)] -pub(crate) struct InclusionProbabilities { - pub inclusion_probabilities: Vec, - pub samples: u64, - pub elapsed: Duration, - pub delta_max: f64, - pub delta_l2: f64, -} - -impl InclusionProbabilities { - pub fn node(&self, mix_id: MixId) -> Option<&InclusionProbability> { - self.inclusion_probabilities - .iter() - .find(|x| x.mix_id == mix_id) - } -} - impl NodeStatusCache { fn new() -> Self { NodeStatusCache { @@ -66,9 +63,18 @@ impl NodeStatusCache { }) } - async fn update_cache(&self, inclusion_probabilities: InclusionProbabilities) { + async fn update_cache( + &self, + mixnodes: Vec, + rewarded_set: Vec, + active_set: Vec, + inclusion_probabilities: InclusionProbabilities, + ) { match time::timeout(Duration::from_millis(CACHE_TIMOUT_MS), self.inner.write()).await { Ok(mut cache) => { + cache.mixnodes_annotated.update(mixnodes); + cache.rewarded_set_annotated.update(rewarded_set); + cache.active_set_annotated.update(active_set); cache .inclusion_probabilities .update(inclusion_probabilities); @@ -77,45 +83,93 @@ impl NodeStatusCache { } } - pub(crate) async fn inclusion_probabilities(&self) -> Option> { + async fn get_cache( + &self, + fn_arg: impl FnOnce(RwLockReadGuard<'_, NodeStatusCacheInner>) -> Cache, + ) -> Option> { match time::timeout(Duration::from_millis(CACHE_TIMOUT_MS), self.inner.read()).await { - Ok(cache) => Some(cache.inclusion_probabilities.clone()), + Ok(cache) => Some(fn_arg(cache)), Err(e) => { error!("{e}"); None } } } + + pub(crate) async fn mixnodes_annotated(&self) -> Option>> { + self.get_cache(|c| c.mixnodes_annotated.clone()).await + } + + pub(crate) async fn rewarded_set_annotated(&self) -> Option>> { + self.get_cache(|c| c.rewarded_set_annotated.clone()).await + } + + pub(crate) async fn active_set_annotated(&self) -> Option>> { + self.get_cache(|c| c.active_set_annotated.clone()).await + } + + pub(crate) async fn inclusion_probabilities(&self) -> Option> { + self.get_cache(|c| c.inclusion_probabilities.clone()).await + } + + pub async fn mixnode_details( + &self, + mix_id: MixId, + ) -> (Option, MixnodeStatus) { + // it might not be the most optimal to possibly iterate the entire vector to find (or not) + // the relevant value. However, the vectors are relatively small (< 10_000 elements, < 1000 for active set) + + let active_set = &self.active_set_annotated().await.unwrap().into_inner(); + if let Some(bond) = active_set.iter().find(|mix| mix.mix_id() == mix_id) { + return (Some(bond.clone()), MixnodeStatus::Active); + } + + let rewarded_set = &self.rewarded_set_annotated().await.unwrap().into_inner(); + if let Some(bond) = rewarded_set.iter().find(|mix| mix.mix_id() == mix_id) { + return (Some(bond.clone()), MixnodeStatus::Standby); + } + + let all_bonded = &self.mixnodes_annotated().await.unwrap().into_inner(); + if let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) { + (Some(bond.clone()), MixnodeStatus::Inactive) + } else { + (None, MixnodeStatus::NotFound) + } + } } impl NodeStatusCacheInner { pub fn new() -> Self { - Self { - inclusion_probabilities: Default::default(), - } + Self::default() } } // Long running task responsible of keeping the cache up-to-date. pub struct NodeStatusCacheRefresher { + // Main stored data cache: NodeStatusCache, + fallback_caching_interval: Duration, + + // Sources for when refreshing data contract_cache: ValidatorCache, contract_cache_listener: watch::Receiver, - fallback_caching_interval: Duration, + storage: Option, } impl NodeStatusCacheRefresher { pub(crate) fn new( cache: NodeStatusCache, + fallback_caching_interval: Duration, contract_cache: ValidatorCache, contract_cache_listener: watch::Receiver, - fallback_caching_interval: Duration, + storage: Option, ) -> Self { Self { cache, + fallback_caching_interval, contract_cache, contract_cache_listener, - fallback_caching_interval, + storage, } } @@ -176,78 +230,154 @@ impl NodeStatusCacheRefresher { async fn refresh_cache(&self) -> Result<(), NodeStatusCacheError> { log::info!("Updating node status cache"); - let mixnode_bonds = self.contract_cache.mixnodes().await; - let params = self - .contract_cache - .interval_reward_params() - .await - .into_inner() - .ok_or(NodeStatusCacheError::SimulationFailed)?; - let inclusion_probabilities = compute_inclusion_probabilities(&mixnode_bonds, params) - .ok_or_else(|| { - error!( - "Failed to simulate selection probabilties for mixnodes, not updating cache" - ); - NodeStatusCacheError::SimulationFailed - })?; - self.cache.update_cache(inclusion_probabilities).await; + // Fetch contract cache data to work with + let mixnode_details = self.contract_cache.mixnodes().await; + let interval_reward_params = self.contract_cache.interval_reward_params().await; + let current_interval = self.contract_cache.current_interval().await; + + let rewarded_set = self.contract_cache.rewarded_set().await; + let active_set = self.contract_cache.active_set().await; + + let interval_reward_params = + interval_reward_params.ok_or(NodeStatusCacheError::SourceDataMissing)?; + let current_interval = current_interval.ok_or(NodeStatusCacheError::SourceDataMissing)?; + + // Compute inclusion probabilities + let inclusion_probabilities = InclusionProbabilities::compute( + &mixnode_details, + interval_reward_params, + ) + .ok_or_else(|| { + error!("Failed to simulate selection probabilties for mixnodes, not updating cache"); + NodeStatusCacheError::SimulationFailed + })?; + + // Create annotated data + let rewarded_set_node_status = to_rewarded_set_node_status(&rewarded_set, &active_set); + let mixnodes_annotated = self + .annotate_node_with_details( + mixnode_details, + interval_reward_params, + current_interval, + &rewarded_set_node_status, + ) + .await; + + // Create the annotated rewarded and active sets + let (rewarded_set, active_set) = + split_into_active_and_rewarded_set(&mixnodes_annotated, &rewarded_set_node_status); + + self.cache + .update_cache( + mixnodes_annotated, + rewarded_set, + active_set, + inclusion_probabilities, + ) + .await; Ok(()) } + + async fn get_performance_from_storage( + &self, + mix_id: MixId, + epoch: Interval, + ) -> Option { + self.storage + .as_ref()? + .get_average_mixnode_uptime_in_the_last_24hrs( + mix_id, + epoch.current_epoch_end_unix_timestamp(), + ) + .await + .ok() + .map(Into::into) + } + + async fn annotate_node_with_details( + &self, + mixnodes: Vec, + interval_reward_params: RewardingParams, + current_interval: Interval, + rewarded_set: &HashMap, + ) -> Vec { + let mut annotated = Vec::new(); + for mixnode in mixnodes { + let stake_saturation = mixnode + .rewarding_details + .bond_saturation(&interval_reward_params); + + let uncapped_stake_saturation = mixnode + .rewarding_details + .uncapped_bond_saturation(&interval_reward_params); + + // If the performance can't be obtained, because the validator-api was not started with + // the monitoring (and hence, storage), then reward estimates will be all zero + let performance = self + .get_performance_from_storage(mixnode.mix_id(), current_interval) + .await + .unwrap_or_default(); + + let rewarded_set_status = rewarded_set.get(&mixnode.mix_id()).copied(); + + let reward_estimate = compute_reward_estimate( + &mixnode, + performance, + rewarded_set_status, + interval_reward_params, + current_interval, + ); + + let (estimated_operator_apy, estimated_delegators_apy) = + compute_apy_from_reward(&mixnode, reward_estimate, current_interval); + + annotated.push(MixNodeBondAnnotated { + mixnode_details: mixnode, + stake_saturation, + uncapped_stake_saturation, + performance, + estimated_operator_apy, + estimated_delegators_apy, + }); + } + annotated + } } -fn compute_inclusion_probabilities( - mixnodes: &[MixNodeDetails], - params: RewardingParams, -) -> Option { - let active_set_size = params.active_set_size; - let standby_set_size = params.rewarded_set_size - active_set_size; - - // Unzip list of total bonds into ids and bonds. - // We need to go through this zip/unzip procedure to make sure we have matching identities - // for the input to the simulator, which assumes the identity is the position in the vec - let (ids, mixnode_total_bonds) = unzip_into_mixnode_ids_and_total_bonds(mixnodes); - - // Compute inclusion probabilitites and keep track of how long time it took. - let mut rng = rand::thread_rng(); - let results = inclusion_probability::simulate_selection_probability_mixnodes( - &mixnode_total_bonds, - active_set_size as usize, - standby_set_size as usize, - MAX_SIMULATION_SAMPLES, - Duration::from_secs(MAX_SIMULATION_TIME_SEC), - &mut rng, - ) - .tap_err(|err| error!("{err}")) - .ok()?; - - Some(InclusionProbabilities { - inclusion_probabilities: zip_ids_together_with_results(&ids, &results), - samples: results.samples, - elapsed: results.time, - delta_max: results.delta_max, - delta_l2: results.delta_l2, - }) -} - -fn unzip_into_mixnode_ids_and_total_bonds(mixnodes: &[MixNodeDetails]) -> (Vec, Vec) { - mixnodes +fn to_rewarded_set_node_status( + rewarded_set: &[MixNodeDetails], + active_set: &[MixNodeDetails], +) -> HashMap { + let mut rewarded_set_node_status: HashMap = rewarded_set .iter() - .map(|m| (m.mix_id(), truncate_decimal(m.total_stake()).u128())) - .unzip() + .map(|m| (m.mix_id(), RewardedSetNodeStatus::Standby)) + .collect(); + for mixnode in active_set { + *rewarded_set_node_status + .get_mut(&mixnode.mix_id()) + .expect("All active nodes are rewarded nodes") = RewardedSetNodeStatus::Active; + } + rewarded_set_node_status } -fn zip_ids_together_with_results( - ids: &[MixId], - results: &inclusion_probability::SelectionProbability, -) -> Vec { - ids.iter() - .zip(results.active_set_probability.iter()) - .zip(results.reserve_set_probability.iter()) - .map(|((&mix_id, a), r)| InclusionProbability { - mix_id, - in_active: *a, - in_reserve: *r, +fn split_into_active_and_rewarded_set( + mixnodes_annotated: &[MixNodeBondAnnotated], + rewarded_set_node_status: &HashMap, +) -> (Vec, Vec) { + let rewarded_set: Vec<_> = mixnodes_annotated + .iter() + .filter(|mixnode| rewarded_set_node_status.get(&mixnode.mix_id()).is_some()) + .cloned() + .collect(); + let active_set: Vec<_> = rewarded_set + .iter() + .filter(|mixnode| { + rewarded_set_node_status + .get(&mixnode.mix_id()) + .map_or(false, RewardedSetNodeStatus::is_active) }) - .collect() + .cloned() + .collect(); + (rewarded_set, active_set) } diff --git a/validator-api/src/node_status_api/cache/inclusion_probabilities.rs b/validator-api/src/node_status_api/cache/inclusion_probabilities.rs new file mode 100644 index 0000000000..21c1bdcab5 --- /dev/null +++ b/validator-api/src/node_status_api/cache/inclusion_probabilities.rs @@ -0,0 +1,89 @@ +use contracts_common::truncate_decimal; +use mixnet_contract_common::{MixId, MixNodeDetails, RewardingParams}; +use serde::Serialize; +use std::time::Duration; +use tap::TapFallible; +use validator_api_requests::models::InclusionProbability; + +const MAX_SIMULATION_SAMPLES: u64 = 5000; +const MAX_SIMULATION_TIME_SEC: u64 = 15; + +#[derive(Clone, Default, Serialize, schemars::JsonSchema)] +pub(crate) struct InclusionProbabilities { + pub inclusion_probabilities: Vec, + pub samples: u64, + pub elapsed: Duration, + pub delta_max: f64, + pub delta_l2: f64, +} + +impl InclusionProbabilities { + pub(crate) fn compute( + mixnodes: &[MixNodeDetails], + params: RewardingParams, + ) -> Option { + compute_inclusion_probabilities(mixnodes, params) + } + + pub(crate) fn node(&self, mix_id: MixId) -> Option<&InclusionProbability> { + self.inclusion_probabilities + .iter() + .find(|x| x.mix_id == mix_id) + } +} + +fn compute_inclusion_probabilities( + mixnodes: &[MixNodeDetails], + params: RewardingParams, +) -> Option { + let active_set_size = params.active_set_size; + let standby_set_size = params.rewarded_set_size - active_set_size; + + // Unzip list of total bonds into ids and bonds. + // We need to go through this zip/unzip procedure to make sure we have matching identities + // for the input to the simulator, which assumes the identity is the position in the vec + let (ids, mixnode_total_bonds) = unzip_into_mixnode_ids_and_total_bonds(mixnodes); + + // Compute inclusion probabilitites and keep track of how long time it took. + let mut rng = rand::thread_rng(); + let results = inclusion_probability::simulate_selection_probability_mixnodes( + &mixnode_total_bonds, + active_set_size as usize, + standby_set_size as usize, + MAX_SIMULATION_SAMPLES, + Duration::from_secs(MAX_SIMULATION_TIME_SEC), + &mut rng, + ) + .tap_err(|err| error!("{err}")) + .ok()?; + + Some(InclusionProbabilities { + inclusion_probabilities: zip_ids_together_with_results(&ids, &results), + samples: results.samples, + elapsed: results.time, + delta_max: results.delta_max, + delta_l2: results.delta_l2, + }) +} + +fn unzip_into_mixnode_ids_and_total_bonds(mixnodes: &[MixNodeDetails]) -> (Vec, Vec) { + mixnodes + .iter() + .map(|m| (m.mix_id(), truncate_decimal(m.total_stake()).u128())) + .unzip() +} + +fn zip_ids_together_with_results( + ids: &[MixId], + results: &inclusion_probability::SelectionProbability, +) -> Vec { + ids.iter() + .zip(results.active_set_probability.iter()) + .zip(results.reserve_set_probability.iter()) + .map(|((&mix_id, a), r)| InclusionProbability { + mix_id, + in_active: *a, + in_reserve: *r, + }) + .collect() +} diff --git a/validator-api/src/node_status_api/helpers.rs b/validator-api/src/node_status_api/helpers.rs index c0c2a544bd..4b084533bc 100644 --- a/validator-api/src/node_status_api/helpers.rs +++ b/validator-api/src/node_status_api/helpers.rs @@ -1,7 +1,6 @@ // Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::contract_cache::reward_estimate::compute_reward_estimate; use crate::contract_cache::Cache; use crate::node_status_api::models::ErrorResponse; use crate::storage::ValidatorApiStorage; @@ -12,11 +11,14 @@ use mixnet_contract_common::{Interval, MixId, RewardedSetNodeStatus}; use rocket::http::Status; use rocket::State; use validator_api_requests::models::{ - ComputeRewardEstParam, InclusionProbabilityResponse, MixnodeCoreStatusResponse, - MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, - RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, + AllInclusionProbabilitiesResponse, ComputeRewardEstParam, InclusionProbabilityResponse, + MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, + MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, + StakeSaturationResponse, UptimeResponse, }; +use super::reward_estimate::compute_reward_estimate; + pub(crate) async fn _mixnode_report( storage: &ValidatorApiStorage, mix_id: MixId, @@ -62,17 +64,18 @@ pub(crate) async fn _get_mixnode_status( } pub(crate) async fn _get_mixnode_reward_estimation( - cache: &State, + cache: &State, + validator_cache: &State, mix_id: MixId, ) -> Result { let (mixnode, status) = cache.mixnode_details(mix_id).await; if let Some(mixnode) = mixnode { - let reward_params = cache.interval_reward_params().await; + let reward_params = validator_cache.interval_reward_params().await; let as_at = reward_params.timestamp(); let reward_params = reward_params .into_inner() .ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?; - let current_interval = cache + let current_interval = validator_cache .current_interval() .await .into_inner() @@ -117,17 +120,18 @@ async fn average_mixnode_performance( pub(crate) async fn _compute_mixnode_reward_estimation( user_reward_param: ComputeRewardEstParam, - cache: &ValidatorCache, + cache: &NodeStatusCache, + validator_cache: &ValidatorCache, mix_id: MixId, ) -> Result { let (mixnode, actual_status) = cache.mixnode_details(mix_id).await; if let Some(mut mixnode) = mixnode { - let reward_params = cache.interval_reward_params().await; + let reward_params = validator_cache.interval_reward_params().await; let as_at = reward_params.timestamp(); let reward_params = reward_params .into_inner() .ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?; - let current_interval = cache + let current_interval = validator_cache .current_interval() .await .into_inner() @@ -200,14 +204,15 @@ pub(crate) async fn _compute_mixnode_reward_estimation( } pub(crate) async fn _get_mixnode_stake_saturation( - cache: &ValidatorCache, + cache: &NodeStatusCache, + validator_cache: &ValidatorCache, mix_id: MixId, ) -> Result { let (mixnode, _) = cache.mixnode_details(mix_id).await; if let Some(mixnode) = mixnode { // Recompute the stake saturation just so that we can confidently state that the `as_at` // field is consistent and correct. Luckily this is very cheap. - let reward_params = cache.interval_reward_params().await; + let reward_params = validator_cache.interval_reward_params().await; let as_at = reward_params.timestamp(); let rewarding_params = reward_params .into_inner() @@ -266,3 +271,51 @@ pub(crate) async fn _get_mixnode_avg_uptime( avg_uptime: performance.round_to_integer(), }) } + +pub(crate) async fn _get_mixnode_inclusion_probabilities( + cache: &NodeStatusCache, +) -> Result { + if let Some(prob) = cache.inclusion_probabilities().await { + let as_at = prob.timestamp(); + let prob = prob.into_inner(); + Ok(AllInclusionProbabilitiesResponse { + inclusion_probabilities: prob.inclusion_probabilities, + samples: prob.samples, + elapsed: prob.elapsed, + delta_max: prob.delta_max, + delta_l2: prob.delta_l2, + as_at, + }) + } else { + Err(ErrorResponse::new( + "No data available".to_string(), + Status::ServiceUnavailable, + )) + } +} + +pub(crate) async fn _get_mixnodes_detailed(cache: &NodeStatusCache) -> Vec { + cache + .mixnodes_annotated() + .await + .unwrap_or_default() + .into_inner() +} + +pub(crate) async fn _get_rewarded_set_detailed( + cache: &NodeStatusCache, +) -> Vec { + cache + .rewarded_set_annotated() + .await + .unwrap_or_default() + .into_inner() +} + +pub(crate) async fn _get_active_set_detailed(cache: &NodeStatusCache) -> Vec { + cache + .active_set_annotated() + .await + .unwrap_or_default() + .into_inner() +} diff --git a/validator-api/src/node_status_api/mod.rs b/validator-api/src/node_status_api/mod.rs index 08cf612dba..cbcf1784f2 100644 --- a/validator-api/src/node_status_api/mod.rs +++ b/validator-api/src/node_status_api/mod.rs @@ -10,6 +10,7 @@ pub(crate) mod cache; pub(crate) mod helpers; pub(crate) mod local_guard; pub(crate) mod models; +pub(crate) mod reward_estimate; pub(crate) mod routes; pub(crate) mod uptime_updater; pub(crate) mod utils; @@ -37,6 +38,9 @@ pub(crate) fn node_status_routes( routes::get_mixnode_inclusion_probability, routes::get_mixnode_avg_uptime, routes::get_mixnode_inclusion_probabilities, + routes::get_mixnodes_detailed, + routes::get_rewarded_set_detailed, + routes::get_active_set_detailed, ] } else { // in the minimal variant we would not have access to endpoints relying on existence @@ -46,6 +50,9 @@ pub(crate) fn node_status_routes( routes::get_mixnode_stake_saturation, routes::get_mixnode_inclusion_probability, routes::get_mixnode_inclusion_probabilities, + routes::get_mixnodes_detailed, + routes::get_rewarded_set_detailed, + routes::get_active_set_detailed, ] } } diff --git a/validator-api/src/contract_cache/reward_estimate.rs b/validator-api/src/node_status_api/reward_estimate.rs similarity index 96% rename from validator-api/src/contract_cache/reward_estimate.rs rename to validator-api/src/node_status_api/reward_estimate.rs index 41e285a0a0..33bbd8320c 100644 --- a/validator-api/src/contract_cache/reward_estimate.rs +++ b/validator-api/src/node_status_api/reward_estimate.rs @@ -7,7 +7,7 @@ use mixnet_contract_common::reward_params::{NodeRewardParams, Performance, Rewar use mixnet_contract_common::rewarding::RewardEstimate; use mixnet_contract_common::{Interval, RewardedSetNodeStatus}; -pub fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) -> Decimal { +fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) -> Decimal { if pledge_amount.is_zero() { return Decimal::zero(); } diff --git a/validator-api/src/node_status_api/routes.rs b/validator-api/src/node_status_api/routes.rs index 7665f1bac1..037ca7b117 100644 --- a/validator-api/src/node_status_api/routes.rs +++ b/validator-api/src/node_status_api/routes.rs @@ -3,9 +3,10 @@ use super::NodeStatusCache; use crate::node_status_api::helpers::{ - _compute_mixnode_reward_estimation, _get_mixnode_avg_uptime, - _get_mixnode_inclusion_probability, _get_mixnode_reward_estimation, - _get_mixnode_stake_saturation, _get_mixnode_status, _mixnode_core_status_count, + _compute_mixnode_reward_estimation, _get_active_set_detailed, _get_mixnode_avg_uptime, + _get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability, + _get_mixnode_reward_estimation, _get_mixnode_stake_saturation, _get_mixnode_status, + _get_mixnodes_detailed, _get_rewarded_set_detailed, _mixnode_core_status_count, _mixnode_report, _mixnode_uptime_history, }; use crate::node_status_api::models::ErrorResponse; @@ -19,9 +20,9 @@ use rocket_okapi::openapi; use validator_api_requests::models::{ AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse, - MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, - MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, - UptimeResponse, + MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, + MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, + StakeSaturationResponse, UptimeResponse, }; #[openapi(tag = "status")] @@ -112,10 +113,13 @@ pub(crate) async fn get_mixnode_status( #[openapi(tag = "status")] #[get("/mixnode//reward-estimation")] pub(crate) async fn get_mixnode_reward_estimation( - cache: &State, + cache: &State, + validator_cache: &State, mix_id: MixId, ) -> Result, ErrorResponse> { - Ok(Json(_get_mixnode_reward_estimation(cache, mix_id).await?)) + Ok(Json( + _get_mixnode_reward_estimation(cache, validator_cache, mix_id).await?, + )) } #[openapi(tag = "status")] @@ -125,21 +129,31 @@ pub(crate) async fn get_mixnode_reward_estimation( )] pub(crate) async fn compute_mixnode_reward_estimation( user_reward_param: Json, - cache: &State, + cache: &State, + validator_cache: &State, mix_id: MixId, ) -> Result, ErrorResponse> { Ok(Json( - _compute_mixnode_reward_estimation(user_reward_param.into_inner(), cache, mix_id).await?, + _compute_mixnode_reward_estimation( + user_reward_param.into_inner(), + cache, + validator_cache, + mix_id, + ) + .await?, )) } #[openapi(tag = "status")] #[get("/mixnode//stake-saturation")] pub(crate) async fn get_mixnode_stake_saturation( - cache: &State, + cache: &State, + validator_cache: &State, mix_id: MixId, ) -> Result, ErrorResponse> { - Ok(Json(_get_mixnode_stake_saturation(cache, mix_id).await?)) + Ok(Json( + _get_mixnode_stake_saturation(cache, validator_cache, mix_id).await?, + )) } #[openapi(tag = "status")] @@ -168,21 +182,29 @@ pub(crate) async fn get_mixnode_avg_uptime( pub(crate) async fn get_mixnode_inclusion_probabilities( cache: &State, ) -> Result, ErrorResponse> { - if let Some(prob) = cache.inclusion_probabilities().await { - let as_at = prob.timestamp(); - let prob = prob.into_inner(); - Ok(Json(AllInclusionProbabilitiesResponse { - inclusion_probabilities: prob.inclusion_probabilities, - samples: prob.samples, - elapsed: prob.elapsed, - delta_max: prob.delta_max, - delta_l2: prob.delta_l2, - as_at, - })) - } else { - Err(ErrorResponse::new( - "No data available".to_string(), - Status::ServiceUnavailable, - )) - } + Ok(Json(_get_mixnode_inclusion_probabilities(cache).await?)) +} + +#[openapi(tag = "status")] +#[get("/mixnodes/detailed")] +pub async fn get_mixnodes_detailed( + cache: &State, +) -> Json> { + Json(_get_mixnodes_detailed(cache).await) +} + +#[openapi(tag = "status")] +#[get("/mixnodes/rewarded/detailed")] +pub async fn get_rewarded_set_detailed( + cache: &State, +) -> Json> { + Json(_get_rewarded_set_detailed(cache).await) +} + +#[openapi(tag = "status")] +#[get("/mixnodes/active/detailed")] +pub async fn get_active_set_detailed( + cache: &State, +) -> Json> { + Json(_get_active_set_detailed(cache).await) }