diff --git a/.github/workflows/publish-nym-binaries.yml b/.github/workflows/publish-nym-binaries.yml index e64825e425..b28f118ec7 100644 --- a/.github/workflows/publish-nym-binaries.yml +++ b/.github/workflows/publish-nym-binaries.yml @@ -55,6 +55,7 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable + override: true - name: Build all binaries uses: actions-rs/cargo@v1 diff --git a/.github/workflows/publish-nym-contracts.yml b/.github/workflows/publish-nym-contracts.yml index 85f1363b9b..ed1141ddf1 100644 --- a/.github/workflows/publish-nym-contracts.yml +++ b/.github/workflows/publish-nym-contracts.yml @@ -14,13 +14,14 @@ jobs: - name: Install Rust stable uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: 1.77 target: wasm32-unknown-unknown override: true - components: rustfmt, clippy - name: Install wasm-opt - run: cargo install --version 0.114.0 wasm-opt + uses: ./.github/actions/install-wasm-opt + with: + version: '114' - name: Build release contracts run: make contracts diff --git a/.github/workflows/push-node-status-agent.yaml b/.github/workflows/push-node-status-agent.yaml index d64b32d6b1..9f88f5c162 100644 --- a/.github/workflows/push-node-status-agent.yaml +++ b/.github/workflows/push-node-status-agent.yaml @@ -58,4 +58,4 @@ jobs: - name: BuildAndPushImageOnHarbor run: | docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} - docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fdec67361..5618fe59c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,198 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2024.13-magura] (2024-11-18) + +- Limit race probability ([#5145]) +- bugifx: assign 'node_id' when converting from 'GatewayDetails' to 'TestNode' ([#5143]) +- bugfix: make sure to assign correct node_id and identity during 'gateway_details' table migration ([#5142]) +- Respond to auth messages with same version ([#5140]) +- Pain/polyfill deprecated endpoints ([#5131]) +- change: dont select mixnodes bonded with vested tokens into the rewarded set ([#5129]) +- nym-credential-proxy-requests: reqwest use rustls-tls ([#5116]) +- bugfix: preserve as much as possible of the rewarded set during migration ([#5103]) +- Feature/force refresh node ([#5101]) +- Add NYM_VPN_API to env files ([#5099]) +- bugfix: fixed historical uptimes for nodes ([#5097]) +- Remove old use of 1GB constant ([#5096]) +- Graceful agent 1.1.5 ([#5093]) +- Add more translations from v2 to v3 authenticator ([#5091]) +- Nym node - Fix claim delegator rewards ([#5090]) +- Make 250 GB/30 days for free ride mode ([#5083]) +- Don't increase bandwidth two times ([#5081]) +- Fix expiration date as today + 7 days ([#5076]) +- Fix gateway decreasing bandwidth ([#5075]) +- Allow custom http port to be reset ([#5073]) +- bugfix: additional checks inside credential proxy ([#5072]) +- chore: deprecated old nym-api client methods and replaced them when possible ([#5069]) +- NS API with directory v2 (#5058) ([#5068]) +- bugfix: credential-proxy obtain-async ([#5067]) +- Allow nym node config updates ([#5066]) +- bugfix: use corrext axum extractors for ecash route arguments ([#5065]) +- Merge2/release/2024.13 magura ([#5063]) +- bugfix/feature: added NymApiClient method to get all skimmed nodes ([#5062]) +- Merge1/release/2024.13 magura ([#5061]) +- added hacky routes to return nymnodes alongside legacy nodes ([#5051]) +- bugfix: mark migrated gateways as rewarded in the previous epoch in case theyre in the rewarded set ([#5049]) +- bugfix: adjust runtime storage migration ([#5047]) +- bugfix: supersede 'cb13be27f8f61d9ae74d924e85d2e6787895eb14' by using… ([#5046]) +- bugfix: restore default http port for nym-api ([#5045]) +- bugfix: fix ecash handlers routes ([#5043]) +- bugfix: don't assign exit gateways to standby set ([#5041]) +- bugfix: make sure nym-nodes are also tested by network monitor ([#5040]) +- bugfix: use bonded nym-nodes for determining initial network monitor … ([#5039]) +- bugfix: make gateways insert themselves into [local] topology ([#5038]) +- Pass poisson flag ([#5037]) +- bugfix: use human readable roles for annotations ([#5036]) +- bugfix: use old name for 'epoch_role' in SkimmedNode ([#5034]) +- bugfix: make sure to use correct highest node id when assigning role ([#5032]) +- feature: use axum_client_ip for attempting to extract source ip ([#5031]) +- bugfix: fixed backwards incompatibility for /gateways/described endpoint ([#5030]) +- bugfix: verifying signed information of legacy nodes ([#5029]) +- bugfix: introduce 'LegacyPendingMixNodeChanges' that does not contain 'cost_params_change' ([#5028]) +- bugfix: missing #[serde(default)] for announce port ([#5024]) +- bugfix: directory v2.1 `get_all_avg_gateway_reliability_in_interval` query ([#5023]) +- added 'get_all_described_nodes' to NymApiClient and adjusted return t… ([#5016]) +- Reapply fixes to new branch ([#5014]) +- Consume only positive bandwidth ([#5013]) +- feature: adjusted ticket sizes to the agreed amounts ([#5009]) +- Push private ip before inserting ([#5008]) +- chore: update itertools in compact ecash ([#4994]) +- feature: make accepting t&c a hard requirement for rewarded set selection ([#4993]) +- Fix rustfmt in nym-credential-proxy ([#4992]) +- bugfix: client memory leak ([#4991]) +- Eliminate 0 bandwidth race check ([#4988]) +- [DOCs;/operators]: Release notes for v2024.12 aero ([#4984]) +- Add topup req constructor ([#4983]) +- Fix critical issues SI86 and SI87 from Cure53 ([#4982]) +- Rename nym-vpn-api to nym-credential-proxy ([#4981]) +- enable global ecash routes even if api is not a signer ([#4980]) +- resolve beta clippy issues in contracts ([#4978]) +- Re-enable vested delegation migration ([#4977]) +- feature: require reporting using nym-node binary for rewarded set selection ([#4976]) +- Top up bandwidth ([#4975]) +- [Product Data] Add session type based on ecash ticket received ([#4974]) +- Bugfix/additional directory fixes ([#4973]) +- feat: add Dockerfile for nym node ([#4972]) +- chore: remove unused rocket code ([#4968]) +- Import nym-vpn-api crates ([#4967]) +- feature: importer-cli to correctly handle mixnet/vesting import ([#4966]) +- bugfix: fix expected return type on /v1/gateways endpoint ([#4965]) +- [Product Data] First step in gateway usage data collection ([#4963]) +- Bump sqlx to 0.7.4 ([#4959]) +- Add env feature to clap and make clap parameters available as env variables ([#4957]) +- Feature/contract state tools ([#4954]) +- expose authenticator address along other address in node-details ([#4953]) +- Extract packet processing from mixnode-common ([#4949]) +- nym-api container ([#4948]) +- Ticket type storage ([#4947]) +- Add "utoipa" feature to nym-node ([#4945]) +- build(deps): bump the patch-updates group across 1 directory with 9 updates ([#4944]) +- V2 performance monitoring feature flag ([#4943]) +- Bugfix/rewarder post pruning adjustments ([#4942]) +- Switch over the last set of jobs to arc runners ([#4938]) +- Fix broken build after merge ([#4937]) +- bugfix: correctly paginate through 'search_tx' endpoint ([#4936]) +- Add more conversions for responses of authenticator messages ([#4929]) +- Directory Sevices v2.1 ([#4903]) +- Migrate Legacy Node (Frontend) ([#4826]) +- Fix critical issues SI84 and SI85 from Cure53 ([#4758]) + +[#5145]: https://github.com/nymtech/nym/pull/5145 +[#5143]: https://github.com/nymtech/nym/pull/5143 +[#5142]: https://github.com/nymtech/nym/pull/5142 +[#5140]: https://github.com/nymtech/nym/pull/5140 +[#5131]: https://github.com/nymtech/nym/pull/5131 +[#5129]: https://github.com/nymtech/nym/pull/5129 +[#5116]: https://github.com/nymtech/nym/pull/5116 +[#5103]: https://github.com/nymtech/nym/pull/5103 +[#5101]: https://github.com/nymtech/nym/pull/5101 +[#5099]: https://github.com/nymtech/nym/pull/5099 +[#5097]: https://github.com/nymtech/nym/pull/5097 +[#5096]: https://github.com/nymtech/nym/pull/5096 +[#5093]: https://github.com/nymtech/nym/pull/5093 +[#5091]: https://github.com/nymtech/nym/pull/5091 +[#5090]: https://github.com/nymtech/nym/pull/5090 +[#5083]: https://github.com/nymtech/nym/pull/5083 +[#5081]: https://github.com/nymtech/nym/pull/5081 +[#5076]: https://github.com/nymtech/nym/pull/5076 +[#5075]: https://github.com/nymtech/nym/pull/5075 +[#5073]: https://github.com/nymtech/nym/pull/5073 +[#5072]: https://github.com/nymtech/nym/pull/5072 +[#5069]: https://github.com/nymtech/nym/pull/5069 +[#5068]: https://github.com/nymtech/nym/pull/5068 +[#5067]: https://github.com/nymtech/nym/pull/5067 +[#5066]: https://github.com/nymtech/nym/pull/5066 +[#5065]: https://github.com/nymtech/nym/pull/5065 +[#5063]: https://github.com/nymtech/nym/pull/5063 +[#5062]: https://github.com/nymtech/nym/pull/5062 +[#5061]: https://github.com/nymtech/nym/pull/5061 +[#5051]: https://github.com/nymtech/nym/pull/5051 +[#5049]: https://github.com/nymtech/nym/pull/5049 +[#5047]: https://github.com/nymtech/nym/pull/5047 +[#5046]: https://github.com/nymtech/nym/pull/5046 +[#5045]: https://github.com/nymtech/nym/pull/5045 +[#5043]: https://github.com/nymtech/nym/pull/5043 +[#5041]: https://github.com/nymtech/nym/pull/5041 +[#5040]: https://github.com/nymtech/nym/pull/5040 +[#5039]: https://github.com/nymtech/nym/pull/5039 +[#5038]: https://github.com/nymtech/nym/pull/5038 +[#5037]: https://github.com/nymtech/nym/pull/5037 +[#5036]: https://github.com/nymtech/nym/pull/5036 +[#5034]: https://github.com/nymtech/nym/pull/5034 +[#5032]: https://github.com/nymtech/nym/pull/5032 +[#5031]: https://github.com/nymtech/nym/pull/5031 +[#5030]: https://github.com/nymtech/nym/pull/5030 +[#5029]: https://github.com/nymtech/nym/pull/5029 +[#5028]: https://github.com/nymtech/nym/pull/5028 +[#5024]: https://github.com/nymtech/nym/pull/5024 +[#5023]: https://github.com/nymtech/nym/pull/5023 +[#5016]: https://github.com/nymtech/nym/pull/5016 +[#5014]: https://github.com/nymtech/nym/pull/5014 +[#5013]: https://github.com/nymtech/nym/pull/5013 +[#5009]: https://github.com/nymtech/nym/pull/5009 +[#5008]: https://github.com/nymtech/nym/pull/5008 +[#4994]: https://github.com/nymtech/nym/pull/4994 +[#4993]: https://github.com/nymtech/nym/pull/4993 +[#4992]: https://github.com/nymtech/nym/pull/4992 +[#4991]: https://github.com/nymtech/nym/pull/4991 +[#4988]: https://github.com/nymtech/nym/pull/4988 +[#4984]: https://github.com/nymtech/nym/pull/4984 +[#4983]: https://github.com/nymtech/nym/pull/4983 +[#4982]: https://github.com/nymtech/nym/pull/4982 +[#4981]: https://github.com/nymtech/nym/pull/4981 +[#4980]: https://github.com/nymtech/nym/pull/4980 +[#4978]: https://github.com/nymtech/nym/pull/4978 +[#4977]: https://github.com/nymtech/nym/pull/4977 +[#4976]: https://github.com/nymtech/nym/pull/4976 +[#4975]: https://github.com/nymtech/nym/pull/4975 +[#4974]: https://github.com/nymtech/nym/pull/4974 +[#4973]: https://github.com/nymtech/nym/pull/4973 +[#4972]: https://github.com/nymtech/nym/pull/4972 +[#4968]: https://github.com/nymtech/nym/pull/4968 +[#4967]: https://github.com/nymtech/nym/pull/4967 +[#4966]: https://github.com/nymtech/nym/pull/4966 +[#4965]: https://github.com/nymtech/nym/pull/4965 +[#4963]: https://github.com/nymtech/nym/pull/4963 +[#4959]: https://github.com/nymtech/nym/pull/4959 +[#4957]: https://github.com/nymtech/nym/pull/4957 +[#4954]: https://github.com/nymtech/nym/pull/4954 +[#4953]: https://github.com/nymtech/nym/pull/4953 +[#4949]: https://github.com/nymtech/nym/pull/4949 +[#4948]: https://github.com/nymtech/nym/pull/4948 +[#4947]: https://github.com/nymtech/nym/pull/4947 +[#4945]: https://github.com/nymtech/nym/pull/4945 +[#4944]: https://github.com/nymtech/nym/pull/4944 +[#4943]: https://github.com/nymtech/nym/pull/4943 +[#4942]: https://github.com/nymtech/nym/pull/4942 +[#4938]: https://github.com/nymtech/nym/pull/4938 +[#4937]: https://github.com/nymtech/nym/pull/4937 +[#4936]: https://github.com/nymtech/nym/pull/4936 +[#4929]: https://github.com/nymtech/nym/pull/4929 +[#4903]: https://github.com/nymtech/nym/pull/4903 +[#4826]: https://github.com/nymtech/nym/pull/4826 +[#4758]: https://github.com/nymtech/nym/pull/4758 + ## [2024.12-aero] (2024-10-17) - nym-node: don't use bloomfilters for double spending checks ([#4960]) diff --git a/Cargo.lock b/Cargo.lock index e8a76bdeee..4896aab406 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2428,7 +2428,7 @@ dependencies = [ [[package]] name = "explorer-api" -version = "1.1.41" +version = "1.1.42" dependencies = [ "chrono", "clap 4.5.20", @@ -4451,7 +4451,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nym-api" -version = "1.1.45" +version = "1.1.46" dependencies = [ "anyhow", "async-trait", @@ -4700,7 +4700,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.43" +version = "1.1.44" dependencies = [ "anyhow", "base64 0.22.1", @@ -4781,7 +4781,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.42" +version = "1.1.43" dependencies = [ "bs58", "clap 4.5.20", @@ -5895,7 +5895,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.43" +version = "1.1.44" dependencies = [ "addr", "anyhow", @@ -5946,7 +5946,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.1.9" +version = "1.1.10" dependencies = [ "anyhow", "bip39", @@ -5973,6 +5973,7 @@ dependencies = [ "nym-sphinx-addressing", "nym-task", "nym-types", + "nym-validator-client", "nym-wireguard", "nym-wireguard-types", "rand", @@ -6048,7 +6049,7 @@ dependencies = [ [[package]] name = "nym-node-status-agent" -version = "0.1.4" +version = "0.1.6" dependencies = [ "anyhow", "clap 4.5.20", @@ -6064,7 +6065,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "0.1.5" +version = "0.1.6" dependencies = [ "anyhow", "axum 0.7.7", @@ -6293,7 +6294,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.42" +version = "1.1.43" dependencies = [ "bs58", "clap 4.5.20", @@ -6895,7 +6896,7 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.8" +version = "0.1.9" dependencies = [ "anyhow", "bytes", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index c9317bb5f3..9550c65b3f 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.42" +version = "1.1.43" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 9a22fd4355..5243206efa 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.42" +version = "1.1.43" 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/common/authenticator-requests/src/error.rs b/common/authenticator-requests/src/error.rs index fc9f8700d5..1c43da3cad 100644 --- a/common/authenticator-requests/src/error.rs +++ b/common/authenticator-requests/src/error.rs @@ -22,4 +22,7 @@ pub enum Error { #[error("conversion: {0}")] Conversion(String), + + #[error("failed to serialize response packet: {source}")] + FailedToSerializeResponsePacket { source: Box }, } diff --git a/common/authenticator-requests/src/lib.rs b/common/authenticator-requests/src/lib.rs index 7fd3d7d1fe..27164417e8 100644 --- a/common/authenticator-requests/src/lib.rs +++ b/common/authenticator-requests/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub mod traits; pub mod v1; pub mod v2; pub mod v3; diff --git a/common/authenticator-requests/src/traits.rs b/common/authenticator-requests/src/traits.rs new file mode 100644 index 0000000000..7c7064d174 --- /dev/null +++ b/common/authenticator-requests/src/traits.rs @@ -0,0 +1,343 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::x25519::PrivateKey; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use nym_sphinx::addressing::clients::Recipient; +use nym_wireguard_types::PeerPublicKey; + +use crate::{ + v1, v2, v3, + v4::{self, registration::IpPair}, + Error, +}; + +#[derive(Copy, Clone, Debug)] +pub enum AuthenticatorVersion { + V1, + V2, + V3, + V4, + UNKNOWN, +} + +impl From for AuthenticatorVersion { + fn from(value: Protocol) -> Self { + if value.service_provider_type != ServiceProviderType::Authenticator { + AuthenticatorVersion::UNKNOWN + } else if value.version == v1::VERSION { + AuthenticatorVersion::V1 + } else if value.version == v2::VERSION { + AuthenticatorVersion::V2 + } else if value.version == v3::VERSION { + AuthenticatorVersion::V3 + } else if value.version == v4::VERSION { + AuthenticatorVersion::V4 + } else { + AuthenticatorVersion::UNKNOWN + } + } +} + +pub trait InitMessage { + fn pub_key(&self) -> PeerPublicKey; +} + +impl InitMessage for v1::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v2::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v3::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v4::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +pub trait FinalMessage { + fn pub_key(&self) -> PeerPublicKey; + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>; + fn private_ips(&self) -> IpPair; + fn credential(&self) -> Option; +} + +impl FinalMessage for v1::GatewayClient { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.verify(private_key, nonce) + } + + fn private_ips(&self) -> IpPair { + self.private_ip.into() + } + + fn credential(&self) -> Option { + None + } +} + +impl FinalMessage for v2::registration::FinalMessage { + fn pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + + fn private_ips(&self) -> IpPair { + self.gateway_client.private_ip.into() + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +impl FinalMessage for v3::registration::FinalMessage { + fn pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + + fn private_ips(&self) -> IpPair { + self.gateway_client.private_ip.into() + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +impl FinalMessage for v4::registration::FinalMessage { + fn pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + + fn private_ips(&self) -> IpPair { + self.gateway_client.private_ips + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +pub trait QueryBandwidthMessage { + fn pub_key(&self) -> PeerPublicKey; +} + +impl QueryBandwidthMessage for PeerPublicKey { + fn pub_key(&self) -> PeerPublicKey { + *self + } +} + +pub trait TopUpMessage { + fn pub_key(&self) -> PeerPublicKey; + fn credential(&self) -> CredentialSpendingData; +} + +impl TopUpMessage for v3::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +impl TopUpMessage for v4::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +pub enum AuthenticatorRequest { + Initial { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, + Final { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, + QueryBandwidth { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, + TopUpBandwidth { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, +} + +impl From for AuthenticatorRequest { + fn from(value: v1::request::AuthenticatorRequest) -> Self { + match value.data { + v1::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final { + msg: Box::new(gateway_client), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v2::request::AuthenticatorRequest) -> Self { + match value.data { + v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v3::request::AuthenticatorRequest) -> Self { + match value.data { + v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + v3::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v4::request::AuthenticatorRequest) -> Self { + match value.data { + v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v4::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v4::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + } + } +} diff --git a/common/authenticator-requests/src/v3/conversion.rs b/common/authenticator-requests/src/v3/conversion.rs index 4b43715966..fe0699ab8b 100644 --- a/common/authenticator-requests/src/v3/conversion.rs +++ b/common/authenticator-requests/src/v3/conversion.rs @@ -98,6 +98,16 @@ impl TryFrom for v2::response::Authenticato } } +impl From for v3::response::AuthenticatorResponse { + fn from(value: v2::response::AuthenticatorResponse) -> Self { + Self { + protocol: value.protocol, + data: value.data.into(), + reply_to: value.reply_to, + } + } +} + impl TryFrom for v2::response::AuthenticatorResponseData { type Error = crate::Error; @@ -129,6 +139,22 @@ impl TryFrom for v2::response::Authenti } } +impl From for v3::response::AuthenticatorResponseData { + fn from(value: v2::response::AuthenticatorResponseData) -> Self { + match value { + v2::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(pending_registration_response.into()), + v2::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(registered_response.into()) + } + v2::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(remaining_bandwidth_response.into()), + } + } +} + impl From for v2::response::PendingRegistrationResponse { fn from(value: v3::response::PendingRegistrationResponse) -> Self { Self { @@ -139,6 +165,16 @@ impl From for v2::response::PendingRe } } +impl From for v3::response::PendingRegistrationResponse { + fn from(value: v2::response::PendingRegistrationResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.into(), + } + } +} + impl From for v2::response::RegisteredResponse { fn from(value: v3::response::RegisteredResponse) -> Self { Self { @@ -149,6 +185,16 @@ impl From for v2::response::RegisteredResponse } } +impl From for v3::response::RegisteredResponse { + fn from(value: v2::response::RegisteredResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.into(), + } + } +} + impl From for v2::response::RemainingBandwidthResponse { fn from(value: v3::response::RemainingBandwidthResponse) -> Self { Self { @@ -159,6 +205,16 @@ impl From for v2::response::RemainingB } } +impl From for v3::response::RemainingBandwidthResponse { + fn from(value: v2::response::RemainingBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.map(Into::into), + } + } +} + impl From for v2::registration::RegistrationData { fn from(value: v3::registration::RegistrationData) -> Self { Self { @@ -169,6 +225,16 @@ impl From for v2::registration::Registration } } +impl From for v3::registration::RegistrationData { + fn from(value: v2::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + impl From for v2::registration::RegistredData { fn from(value: v3::registration::RegistredData) -> Self { Self { @@ -179,6 +245,16 @@ impl From for v2::registration::RegistredData { } } +impl From for v3::registration::RegistredData { + fn from(value: v2::registration::RegistredData) -> Self { + Self { + pub_key: value.pub_key, + private_ip: value.private_ip, + wg_port: value.wg_port, + } + } +} + impl From for v2::registration::RemainingBandwidthData { fn from(value: v3::registration::RemainingBandwidthData) -> Self { Self { @@ -186,3 +262,11 @@ impl From for v2::registration::Remain } } } + +impl From for v3::registration::RemainingBandwidthData { + fn from(value: v2::registration::RemainingBandwidthData) -> Self { + Self { + available_bandwidth: value.available_bandwidth, + } + } +} diff --git a/common/authenticator-requests/src/v3/registration.rs b/common/authenticator-requests/src/v3/registration.rs index 70dceb7690..37234f7e1f 100644 --- a/common/authenticator-requests/src/v3/registration.rs +++ b/common/authenticator-requests/src/v3/registration.rs @@ -27,7 +27,7 @@ pub type HmacSha256 = Hmac; pub type Nonce = u64; pub type Taken = Option; -pub const BANDWIDTH_CAP_PER_DAY: u64 = 1024 * 1024 * 1024; // 1 GB +pub const BANDWIDTH_CAP_PER_DAY: u64 = 250 * 1024 * 1024 * 1024; // 250 GB #[derive(Serialize, Deserialize, Debug, Clone)] pub struct InitMessage { diff --git a/common/authenticator-requests/src/v4/conversion.rs b/common/authenticator-requests/src/v4/conversion.rs index 183b3440c3..7b79b5f087 100644 --- a/common/authenticator-requests/src/v4/conversion.rs +++ b/common/authenticator-requests/src/v4/conversion.rs @@ -3,7 +3,7 @@ use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; -use crate::{v3, v4}; +use crate::{v2, v3, v4}; impl From for v4::request::AuthenticatorRequest { fn from(authenticator_request: v3::request::AuthenticatorRequest) -> Self { @@ -64,6 +64,16 @@ impl From> for Box { } } +impl From for v4::registration::GatewayClient { + fn from(gw_client: v2::registration::GatewayClient) -> Self { + Self { + pub_key: gw_client.pub_key, + private_ips: gw_client.private_ip.into(), + mac: gw_client.mac.into(), + } + } +} + impl From for v4::registration::GatewayClient { fn from(gw_client: v3::registration::GatewayClient) -> Self { Self { @@ -84,6 +94,22 @@ impl From for v3::registration::GatewayClient { } } +impl From for v2::registration::GatewayClient { + fn from(gw_client: v4::registration::GatewayClient) -> Self { + Self { + pub_key: gw_client.pub_key, + private_ip: gw_client.private_ips.ipv4.into(), + mac: gw_client.mac.into(), + } + } +} + +impl From for v4::registration::ClientMac { + fn from(mac: v2::registration::ClientMac) -> Self { + Self::new(mac.to_vec()) + } +} + impl From for v4::registration::ClientMac { fn from(mac: v3::registration::ClientMac) -> Self { Self::new(mac.to_vec()) @@ -96,6 +122,12 @@ impl From for v3::registration::ClientMac { } } +impl From for v2::registration::ClientMac { + fn from(mac: v4::registration::ClientMac) -> Self { + Self::new(mac.to_vec()) + } +} + impl TryFrom for v3::response::AuthenticatorResponse { type Error = crate::Error; diff --git a/common/authenticator-requests/src/v4/registration.rs b/common/authenticator-requests/src/v4/registration.rs index a7d5a50f34..a4a49c66a5 100644 --- a/common/authenticator-requests/src/v4/registration.rs +++ b/common/authenticator-requests/src/v4/registration.rs @@ -42,6 +42,12 @@ impl IpPair { } } +impl From<(Ipv4Addr, Ipv6Addr)> for IpPair { + fn from((ipv4, ipv6): (Ipv4Addr, Ipv6Addr)) -> Self { + IpPair { ipv4, ipv6 } + } +} + impl fmt::Display for IpPair { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "({}, {})", self.ipv4, self.ipv6) diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 0e8864edfe..076421bc6a 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -1,7 +1,6 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub use crate::nym_api::NymApiClientExt; use crate::nyxd::{self, NyxdClient}; use crate::signing::direct_wallet::DirectSecp256k1HdWallet; use crate::signing::signer::{NoSigner, OfflineSigner}; @@ -32,6 +31,7 @@ use nym_network_defaults::NymNetworkDetails; use time::Date; use url::Url; +pub use crate::nym_api::NymApiClientExt; pub use nym_mixnet_contract_common::{ mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId, NymNodeDetails, }; @@ -332,10 +332,10 @@ impl NymApiClient { NymApiClient { nym_api } } - pub fn new_with_user_agent(api_url: Url, user_agent: UserAgent) -> Self { + pub fn new_with_user_agent(api_url: Url, user_agent: impl Into) -> Self { let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url) .expect("invalid api url") - .with_user_agent(user_agent) + .with_user_agent(user_agent.into()) .build::() .expect("failed to build nym api client"); diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index d23916bf5b..7e54b3f9ec 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -13,7 +13,7 @@ use nym_api_requests::ecash::models::{ use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ AnnotationResponse, ApiHealthResponse, LegacyDescribedMixNode, NodePerformanceResponse, - NymNodeDescription, + NodeRefreshBody, NymNodeDescription, }; use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse; use nym_api_requests::pagination::PaginatedResponse; @@ -695,16 +695,32 @@ pub trait NymApiClientExt: ApiClient { &self, node_id: NodeId, ) -> Result { - self.get_json_from(format!("/v1/nym-nodes/performance/{node_id}")) - .await + self.get_json( + &[ + routes::API_VERSION, + "nym-nodes", + "performance", + &node_id.to_string(), + ], + NO_PARAMS, + ) + .await } async fn get_node_annotation( &self, node_id: NodeId, ) -> Result { - self.get_json_from(format!("/v1/nym-nodes/annotation/{node_id}")) - .await + self.get_json( + &[ + routes::API_VERSION, + "nym-nodes", + "annotation", + &node_id.to_string(), + ], + NO_PARAMS, + ) + .await } #[deprecated] @@ -917,6 +933,18 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn force_refresh_describe_cache( + &self, + request: &NodeRefreshBody, + ) -> Result<(), NymAPIError> { + self.post_json( + &[routes::API_VERSION, "nym-nodes", "refresh-described"], + NO_PARAMS, + request, + ) + .await + } + async fn issued_ticketbooks_for( &self, expiration_date: Date, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index f42c94cd72..433112e157 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -10,10 +10,10 @@ use cosmrs::AccountId; use nym_contracts_common::signing::Nonce; use nym_mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, PreassignedId}; use nym_mixnet_contract_common::nym_node::{ - EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeOwnershipResponse, - NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, PagedNymNodeDetailsResponse, - PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, StakeSaturationResponse, - UnbondedNodeResponse, UnbondedNymNode, + EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse, + NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, + PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, + StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode, }; use nym_mixnet_contract_common::reward_params::WorkFactor; use nym_mixnet_contract_common::{ @@ -311,10 +311,7 @@ pub trait MixnetQueryClient { .await } - async fn get_nymnode_details( - &self, - node_id: NodeId, - ) -> Result { + async fn get_nymnode_details(&self, node_id: NodeId) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeDetails { node_id }) .await } diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index ce502c112a..549c843958 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -315,6 +315,7 @@ impl Client { parse_response(res, true).await } + #[instrument(level = "debug", skip_all)] pub async fn get_json_endpoint(&self, endpoint: S) -> Result> where for<'a> T: Deserialize<'a>, diff --git a/common/service-provider-requests-common/src/lib.rs b/common/service-provider-requests-common/src/lib.rs index d13a7156c9..f9f0564e1d 100644 --- a/common/service-provider-requests-common/src/lib.rs +++ b/common/service-provider-requests-common/src/lib.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[repr(u8)] pub enum ServiceProviderType { NetworkRequester = 0, diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs index f906c04cd1..3060a02e3a 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -3,7 +3,7 @@ use crate::deprecated::DelegationEvent; use crate::error::TypesError; use crate::mixnode::NodeCostParams; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::{Delegation as MixnetContractDelegation, NodeId}; +use nym_mixnet_contract_common::{Delegation as MixnetContractDelegation, NodeId, NodeRewarding}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -70,6 +70,14 @@ pub struct DelegationWithEverything { pub mixnode_is_unbonding: Option, } +pub struct NodeInformation { + pub owner: String, + pub mix_id: NodeId, + pub node_identity: String, + pub rewarding_details: NodeRewarding, + pub is_unbonding: bool, +} + #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index 0c77bcf010..8c7d94784a 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -7,8 +7,8 @@ use defguard_wireguard_rs::{ WireguardInterfaceApi, }; use futures::channel::oneshot; -use nym_authenticator_requests::{ - latest::registration::RemainingBandwidthData, v1::registration::BANDWIDTH_CAP_PER_DAY, +use nym_authenticator_requests::latest::registration::{ + RemainingBandwidthData, BANDWIDTH_CAP_PER_DAY, }; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig, @@ -194,6 +194,10 @@ impl PeerController { ); self.bw_storage_managers .insert(peer.public_key.clone(), bandwidth_storage_manager); + // try to immediately update the host information, to eliminate races + if let Ok(host_information) = self.wg_api.inner.read_interface_data() { + *self.host_information.write().await = host_information; + } tokio::spawn(async move { if let Err(e) = handle.run().await { log::error!("Peer handle shut down ungracefully - {e}"); @@ -230,7 +234,7 @@ impl PeerController { // host information not updated yet return Ok(None); }; - BANDWIDTH_CAP_PER_DAY.saturating_sub((peer.rx_bytes + peer.tx_bytes) as i64) + BANDWIDTH_CAP_PER_DAY.saturating_sub(peer.rx_bytes + peer.tx_bytes) as i64 }; Ok(Some(RemainingBandwidthData { diff --git a/common/wireguard/src/peer_handle.rs b/common/wireguard/src/peer_handle.rs index 3b6f13d960..71fa06f847 100644 --- a/common/wireguard/src/peer_handle.rs +++ b/common/wireguard/src/peer_handle.rs @@ -6,7 +6,7 @@ use crate::peer_controller::PeerControlRequest; use defguard_wireguard_rs::host::Peer; use defguard_wireguard_rs::{host::Host, key::Key}; use futures::channel::oneshot; -use nym_authenticator_requests::v2::registration::BANDWIDTH_CAP_PER_DAY; +use nym_authenticator_requests::latest::registration::BANDWIDTH_CAP_PER_DAY; use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager; use nym_gateway_storage::models::WireguardPeer; use nym_gateway_storage::Storage; @@ -18,7 +18,7 @@ use tokio::sync::{mpsc, RwLock}; use tokio_stream::{wrappers::IntervalStream, StreamExt}; pub(crate) type SharedBandwidthStorageManager = Arc>>; -const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60 * 24); // 24 hours +const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60 * 24 * 30); // 30 days pub struct PeerHandle { storage: St, @@ -98,7 +98,7 @@ impl PeerHandle { } else { if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER { log::debug!( - "Peer {} has been present for 24 hours, removing it", + "Peer {} has been present for 30 days, removing it", self.public_key.to_string() ); let success = self.remove_peer().await?; diff --git a/contracts/mixnet/src/nodes/storage/helpers.rs b/contracts/mixnet/src/nodes/storage/helpers.rs index 1d15c6b03a..169a0d42bc 100644 --- a/contracts/mixnet/src/nodes/storage/helpers.rs +++ b/contracts/mixnet/src/nodes/storage/helpers.rs @@ -104,7 +104,10 @@ pub(crate) fn next_nymnode_id_counter(store: &mut dyn Storage) -> StdResult Result<(), MixnetContractError> { - ACTIVE_ROLES_BUCKET.save(storage, &RoleStorageBucket::default())?; + let active_bucket = RoleStorageBucket::default(); + let inactive_bucket = active_bucket.other(); + + ACTIVE_ROLES_BUCKET.save(storage, &active_bucket)?; let roles = vec![ Role::Layer1, Role::Layer2, @@ -114,24 +117,12 @@ pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), Mixnet Role::Standby, ]; for role in roles { - ROLES.save(storage, (RoleStorageBucket::default() as u8, role), &vec![])?; - ROLES.save( - storage, - (RoleStorageBucket::default().other() as u8, role), - &vec![], - )? + ROLES.save(storage, (active_bucket as u8, role), &vec![])?; + ROLES.save(storage, (inactive_bucket as u8, role), &vec![])? } - ROLES_METADATA.save( - storage, - RoleStorageBucket::default() as u8, - &Default::default(), - )?; - ROLES_METADATA.save( - storage, - RoleStorageBucket::default().other() as u8, - &Default::default(), - )?; + ROLES_METADATA.save(storage, active_bucket as u8, &Default::default())?; + ROLES_METADATA.save(storage, inactive_bucket as u8, &Default::default())?; Ok(()) } diff --git a/envs/canary.env b/envs/canary.env index db70a19fd0..84aa65eefa 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -18,6 +18,7 @@ GROUP_CONTRACT_ADDRESS=n1qg5ega6dykkxc307y25pecuufrjkxkaggkkxh7nad0vhyhtuhw3sa07 MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqx5a364 COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9 -EXPLORER_API=https://canary-explorer.performance.nymte.ch/api +EXPLORER_API=https://canary-explorer.performance.nymte.ch/api/ NYXD=https://canary-validator.performance.nymte.ch -NYM_API=https://canary-api.performance.nymte.ch/api +NYM_API=https://canary-api.performance.nymte.ch/api/ +NYM_VPN_API=https://nym-vpn-api-git-deploy-canary-nyx-network-staging.vercel.app/api/ diff --git a/envs/qa.env b/envs/qa.env index 34306a62ea..81adaf299c 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -18,6 +18,7 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq683 VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39 -EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api +EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/ NYXD=https://qa-validator.qa.nymte.ch -NYM_API=https://qa-nym-api.qa.nymte.ch/api +NYM_API=https://qa-nym-api.qa.nymte.ch/api/ +NYM_VPN_API=https://nym-vpn-api-git-deploy-qa-nyx-network-staging.vercel.app/api/ diff --git a/envs/sandbox.env b/envs/sandbox.env index 6310b8fa89..3033312368 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -19,7 +19,8 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865 ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9 STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" -EXPLORER_API=https://sandbox-explorer.nymtech.net/api +EXPLORER_API=https://sandbox-explorer.nymtech.net/api/ NYXD=https://rpc.sandbox.nymtech.net -NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket -NYM_API=https://sandbox-nym-api1.nymtech.net/api +NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket/ +NYM_API=https://sandbox-nym-api1.nymtech.net/api/ +NYM_VPN_API=https://nym-vpn-api-git-deploy-sandbox-nyx-network-staging.vercel.app/api/ diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 8e59935476..11f8672f98 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "explorer-api" -version = "1.1.41" +version = "1.1.42" edition = "2021" license.workspace = true diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts index 5375377b5a..91b082c5ca 100644 --- a/explorer/src/utils/index.ts +++ b/explorer/src/utils/index.ts @@ -102,7 +102,6 @@ export const isLessThan = (a: number, b: number) => a < b; */ export const isBalanceEnough = (fee: string, tx: string = '0', balance: string = '0') => { - console.log('balance', balance, fee, tx); try { return Big(balance).gte(Big(fee).plus(Big(tx))); } catch (e) { diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 4ce3a27e34..02abce71e8 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.45" +version = "1.1.46" authors.workspace = true edition = "2021" rust-version.workspace = true diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 9d1bb4a671..ee87d37f74 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -4,6 +4,7 @@ #![allow(deprecated)] use crate::helpers::unix_epoch; +use crate::helpers::PlaceholderJsonSchemaImpl; use crate::legacy::{ LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; @@ -1265,6 +1266,67 @@ pub struct NoiseDetails { pub ip_addresses: Vec, } +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NodeRefreshBody { + #[serde(with = "bs58_ed25519_pubkey")] + #[schemars(with = "String")] + pub node_identity: ed25519::PublicKey, + + // a poor man's nonce + pub request_timestamp: i64, + + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signature: ed25519::Signature, +} + +impl NodeRefreshBody { + pub fn plaintext(node_identity: ed25519::PublicKey, request_timestamp: i64) -> Vec { + node_identity + .to_bytes() + .into_iter() + .chain(request_timestamp.to_be_bytes()) + .chain(b"describe-cache-refresh-request".iter().copied()) + .collect() + } + + pub fn new(private_key: &ed25519::PrivateKey) -> Self { + let node_identity = private_key.public_key(); + let request_timestamp = OffsetDateTime::now_utc().unix_timestamp(); + let signature = private_key.sign(Self::plaintext(node_identity, request_timestamp)); + NodeRefreshBody { + node_identity, + request_timestamp, + signature, + } + } + + pub fn verify_signature(&self) -> bool { + self.node_identity + .verify( + Self::plaintext(self.node_identity, self.request_timestamp), + &self.signature, + ) + .is_ok() + } + + pub fn is_stale(&self) -> bool { + let Ok(encoded) = OffsetDateTime::from_unix_timestamp(self.request_timestamp) else { + return true; + }; + let now = OffsetDateTime::now_utc(); + + if encoded > now { + return true; + } + + if (encoded + Duration::from_secs(30)) < now { + return true; + } + + false + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index 1fc39706b9..1beed24b3d 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1264,6 +1264,7 @@ struct TestFixture { impl TestFixture { fn build_app_state(storage: NymApiStorage) -> AppState { AppState { + forced_refresh: Default::default(), nym_contract_cache: NymContractCache::new(), node_status_cache: NodeStatusCache::new(), circulating_supply_cache: CirculatingSupplyCache::new("unym".to_owned()), diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index 1dec38383a..58a62dc27c 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -227,6 +227,11 @@ impl EpochAdvancer { continue; }; + if mix.bond_information.proxy.is_some() { + debug!("legacy mixnode {node_id} is using vested tokens"); + continue; + } + let performance = annotation.detailed_performance.to_rewarding_performance(); debug!( "legacy mixnode {}: stake: {total_stake}, performance: {performance}", diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index 8368e58a97..aa80d831ab 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -9,9 +9,10 @@ use crate::support::config; use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE; use async_trait::async_trait; use futures::{stream, StreamExt}; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription}; use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; -use nym_mixnet_contract_common::{LegacyMixLayer, NodeId}; +use nym_mixnet_contract_common::{LegacyMixLayer, NodeId, NymNodeDetails}; use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; use nym_topology::gateway::GatewayConversionError; use nym_topology::mix::MixnodeConversionError; @@ -151,6 +152,10 @@ pub struct DescribedNodes { } impl DescribedNodes { + pub fn force_update(&mut self, node: NymNodeDescription) { + self.nodes.insert(node.node_id, node); + } + pub fn get_description(&self, node_id: &NodeId) -> Option<&NymNodeData> { self.nodes.get(node_id).map(|n| &n.description) } @@ -292,7 +297,7 @@ async fn try_get_description( } #[derive(Debug)] -struct RefreshData { +pub(crate) struct RefreshData { host: String, node_id: NodeId, node_type: DescribedNodeType, @@ -300,6 +305,39 @@ struct RefreshData { port: Option, } +impl<'a> From<&'a LegacyMixNodeDetailsWithLayer> for RefreshData { + fn from(node: &'a LegacyMixNodeDetailsWithLayer) -> Self { + RefreshData::new( + &node.bond_information.mix_node.host, + DescribedNodeType::LegacyMixnode, + node.mix_id(), + Some(node.bond_information.mix_node.http_api_port), + ) + } +} + +impl<'a> From<&'a LegacyGatewayBondWithId> for RefreshData { + fn from(node: &'a LegacyGatewayBondWithId) -> Self { + RefreshData::new( + &node.bond.gateway.host, + DescribedNodeType::LegacyGateway, + node.node_id, + None, + ) + } +} + +impl<'a> From<&'a NymNodeDetails> for RefreshData { + fn from(node: &'a NymNodeDetails) -> Self { + RefreshData::new( + &node.bond_information.node.host, + DescribedNodeType::NymNode, + node.node_id(), + node.bond_information.node.custom_http_port, + ) + } +} + impl RefreshData { pub fn new( host: impl Into, @@ -315,7 +353,11 @@ impl RefreshData { } } - async fn try_refresh(self) -> Option { + pub(crate) fn node_id(&self) -> NodeId { + self.node_id + } + + pub(crate) async fn try_refresh(self) -> Option { match try_get_description(self).await { Ok(description) => Some(description), Err(err) => { @@ -341,18 +383,13 @@ impl CacheItemProvider for NodeDescriptionProvider { // - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info) // - nym-nodes - let mut nodes_to_query = Vec::new(); + let mut nodes_to_query: Vec = Vec::new(); match self.contract_cache.all_cached_legacy_mixnodes().await { None => error!("failed to obtain mixnodes information from the cache"), Some(legacy_mixnodes) => { for node in &**legacy_mixnodes { - nodes_to_query.push(RefreshData::new( - &node.bond_information.mix_node.host, - DescribedNodeType::LegacyMixnode, - node.mix_id(), - Some(node.bond_information.mix_node.http_api_port), - )) + nodes_to_query.push(node.into()) } } } @@ -361,12 +398,7 @@ impl CacheItemProvider for NodeDescriptionProvider { None => error!("failed to obtain gateways information from the cache"), Some(legacy_gateways) => { for node in &**legacy_gateways { - nodes_to_query.push(RefreshData::new( - &node.bond.gateway.host, - DescribedNodeType::LegacyGateway, - node.node_id, - None, - )) + nodes_to_query.push(node.into()) } } } @@ -375,12 +407,7 @@ impl CacheItemProvider for NodeDescriptionProvider { None => error!("failed to obtain nym-nodes information from the cache"), Some(nym_nodes) => { for node in &**nym_nodes { - nodes_to_query.push(RefreshData::new( - &node.bond_information.node.host, - DescribedNodeType::NymNode, - node.node_id(), - node.bond_information.node.custom_http_port, - )) + nodes_to_query.push(node.into()) } } } diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 756fe37140..958188db9d 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -356,6 +356,13 @@ impl AxumErrorResponse { } } + pub(crate) fn unauthorised(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::UNAUTHORIZED, + } + } + pub(crate) fn unprocessable_entity(msg: impl Display) -> Self { Self { message: RequestError::new(msg.to_string()), @@ -376,6 +383,13 @@ impl AxumErrorResponse { status: StatusCode::BAD_REQUEST, } } + + pub(crate) fn too_many(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::TOO_MANY_REQUESTS, + } + } } impl From for AxumErrorResponse { diff --git a/nym-api/src/node_status_api/uptime_updater.rs b/nym-api/src/node_status_api/uptime_updater.rs index 67de44839b..affca9f7f2 100644 --- a/nym-api/src/node_status_api/uptime_updater.rs +++ b/nym-api/src/node_status_api/uptime_updater.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::{ @@ -9,7 +9,7 @@ use crate::storage::NymApiStorage; use nym_task::{TaskClient, TaskManager}; use std::time::Duration; use time::{OffsetDateTime, PrimitiveDateTime, Time}; -use tokio::time::{interval, sleep}; +use tokio::time::{interval_at, Instant}; use tracing::error; use tracing::{info, trace, warn}; @@ -93,15 +93,8 @@ impl HistoricalUptimeUpdater { "waiting until {update_datetime} to update the historical uptimes for the first time ({} seconds left)", time_left.as_secs() ); - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("UpdateHandler: Received shutdown"); - } - _ = sleep(time_left) => {} - } - - let mut interval = interval(ONE_DAY); + let start = Instant::now() + time_left; + let mut interval = interval_at(start, ONE_DAY); while !shutdown.is_shutdown() { tokio::select! { biased; @@ -109,6 +102,7 @@ impl HistoricalUptimeUpdater { trace!("UpdateHandler: Received shutdown"); } _ = interval.tick() => { + info!("updating historical uptimes of nodes"); // we don't want to have another select here; uptime update is relatively speedy // and we don't want to exit while we're in the middle of database update if let Err(err) = self.update_uptimes().await { diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index 3692c35480..eeb9b2a7c2 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -1,6 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_describe_cache::RefreshData; use crate::nym_contract_cache::cache::data::CachedContractsInfo; use crate::support::caching::Cache; use data::ContractCacheData; @@ -8,6 +9,7 @@ use nym_api_requests::legacy::{ LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; use nym_api_requests::models::MixnodeStatus; +use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::{ ConfigScoreParams, Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams, }; @@ -364,6 +366,48 @@ impl NymContractCache { self.legacy_mixnode_details(mix_id).await.1 } + pub async fn get_node_refresh_data( + &self, + node_identity: ed25519::PublicKey, + ) -> Option { + if !self.initialised() { + return None; + } + + let inner = self.inner.read().await; + + let encoded_identity = node_identity.to_base58_string(); + + // 1. check nymnodes + if let Some(nym_node) = inner + .nym_nodes + .iter() + .find(|n| n.bond_information.identity() == encoded_identity) + { + return Some(nym_node.into()); + } + + // 2. check legacy mixnodes + if let Some(mixnode) = inner + .legacy_mixnodes + .iter() + .find(|n| n.bond_information.identity() == encoded_identity) + { + return Some(mixnode.into()); + } + + // 3. check legacy gateways + if let Some(gateway) = inner + .legacy_gateways + .iter() + .find(|n| n.identity() == &encoded_identity) + { + return Some(gateway.into()); + } + + None + } + pub fn initialised(&self) -> bool { self.initialised.load(Ordering::Relaxed) } diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/nym_contract_cache/handlers.rs index d57f1317c4..00f02e1fe2 100644 --- a/nym-api/src/nym_contract_cache/handlers.rs +++ b/nym-api/src/nym_contract_cache/handlers.rs @@ -6,10 +6,12 @@ use crate::node_status_api::helpers::{ _get_rewarded_set_legacy_mixnodes_detailed, }; use crate::support::http::state::AppState; +use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; use axum::extract::State; use axum::{Json, Router}; use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use nym_api_requests::models::MixNodeBondAnnotated; +use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; use std::collections::HashSet; @@ -58,11 +60,47 @@ pub(crate) fn nym_contract_cache_routes() -> Router { )] #[deprecated] async fn get_mixnodes(State(state): State) -> Json> { - state - .nym_contract_cache() - .legacy_mixnodes_filtered() - .await - .into() + let mut out = state.nym_contract_cache().legacy_mixnodes_filtered().await; + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a mixnode, ignore it + if !description.declared_role.mixnode { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy mixnode being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + + let node = to_legacy_mixnode(nym_node, description); + out.push(node); + } + + Json(out) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -97,15 +135,54 @@ async fn get_mixnodes_detailed(State(state): State) -> Json) -> Json> { - Json( - state - .nym_contract_cache() - .legacy_gateways_filtered() - .await - .into_iter() - .map(Into::into) - .collect(), - ) + // legacy + let mut out: Vec = state + .nym_contract_cache() + .legacy_gateways_filtered() + .await + .into_iter() + .map(Into::into) + .collect(); + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.entry { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy gateway being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + + let node = to_legacy_gateway(nym_node, description); + out.push(node); + } + + Json(out) } #[utoipa::path( @@ -166,12 +243,59 @@ async fn get_rewarded_set_detailed( )] #[deprecated] async fn get_active_set(State(state): State) -> Json> { - state + let mut out = state .nym_contract_cache() .legacy_v1_active_set_mixnodes() .await - .clone() - .into() + .clone(); + + let Some(rewarded_set) = state.nym_contract_cache().rewarded_set().await else { + return Json(out); + }; + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a mixnode, ignore it + if !description.declared_role.mixnode { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy mixnode being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + // if the node is not in the active set, ignore it + if !rewarded_set.is_active_mixnode(&nym_node.node_id()) { + continue; + } + + let node = to_legacy_mixnode(nym_node, description); + out.push(node); + } + + Json(out) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, diff --git a/nym-api/src/nym_nodes/handlers/legacy.rs b/nym-api/src/nym_nodes/handlers/legacy.rs index cae94d5254..3dd6a51f74 100644 --- a/nym-api/src/nym_nodes/handlers/legacy.rs +++ b/nym-api/src/nym_nodes/handlers/legacy.rs @@ -2,8 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::http::state::AppState; +use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; use axum::extract::State; use axum::{Json, Router}; +use nym_api_requests::legacy::LegacyMixNodeBondWithLayer; use nym_api_requests::models::{LegacyDescribedGateway, LegacyDescribedMixNode}; // we want to mark the routes as deprecated in swagger, but still expose them @@ -34,25 +36,44 @@ async fn get_gateways_described( ) -> Json> { let contract_cache = state.nym_contract_cache(); let describe_cache = state.described_nodes_cache(); - let gateways = contract_cache.legacy_gateways_filtered().await; - if gateways.is_empty() { - return Json(Vec::new()); - } + + // legacy + let legacy = contract_cache.legacy_gateways_filtered().await; // if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(gateways.into_iter().map(Into::into).collect()); + let Ok(describe_cache) = describe_cache.get().await else { + return Json(legacy.into_iter().map(Into::into).collect()); }; - Json( - gateways - .into_iter() - .map(|bond| LegacyDescribedGateway { - self_described: self_descriptions.get_description(&bond.node_id).cloned(), - bond: bond.bond, - }) - .collect(), - ) + let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; + let mut out = Vec::new(); + + for legacy_bond in legacy { + out.push(LegacyDescribedGateway { + self_described: describe_cache + .get_description(&legacy_bond.node_id) + .cloned(), + bond: legacy_bond.bond, + }) + } + + for nym_node in migrated_nymnodes { + // we ALWAYS need description to set legacy fields + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.entry { + continue; + } + + out.push(LegacyDescribedGateway { + bond: to_legacy_gateway(&nym_node, description), + self_described: Some(description.clone()), + }) + } + + Json(out) } #[utoipa::path( @@ -70,28 +91,43 @@ async fn get_mixnodes_described( let contract_cache = state.nym_contract_cache(); let describe_cache = state.described_nodes_cache(); - let mixnodes = contract_cache + let legacy: Vec = contract_cache .legacy_mixnodes_filtered() .await .into_iter() .map(|m| m.bond_information) .collect::>(); - if mixnodes.is_empty() { - return Json(Vec::new()); - } - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(mixnodes.into_iter().map(Into::into).collect()); + // if the self describe cache is unavailable, well, don't attach describe data and only return legacy mixnodes + let Ok(describe_cache) = describe_cache.get().await else { + return Json(legacy.into_iter().map(Into::into).collect()); }; - Json( - mixnodes - .into_iter() - .map(|bond| LegacyDescribedMixNode { - self_described: self_descriptions.get_description(&bond.mix_id).cloned(), - bond, - }) - .collect(), - ) + let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; + let mut out = Vec::new(); + + for legacy_bond in legacy { + out.push(LegacyDescribedMixNode { + self_described: describe_cache.get_description(&legacy_bond.mix_id).cloned(), + bond: legacy_bond, + }) + } + + for nym_node in migrated_nymnodes { + // we ALWAYS need description to set legacy fields + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.mixnode { + continue; + } + + out.push(LegacyDescribedMixNode { + bond: to_legacy_mixnode(&nym_node, description).bond_information, + self_described: Some(description.clone()), + }) + } + + Json(out) } diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index 4ccb3c02de..47530a6c5c 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -5,11 +5,11 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::helpers::{NodeIdParam, PaginationRequest}; use crate::support::http::state::AppState; use axum::extract::{Path, Query, State}; -use axum::routing::get; +use axum::routing::{get, post}; use axum::{Json, Router}; use nym_api_requests::models::{ - AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NoiseDetails, - NymNodeDescription, PerformanceHistoryResponse, UptimeHistoryResponse, + AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NodeRefreshBody, + NoiseDetails, NymNodeDescription, PerformanceHistoryResponse, UptimeHistoryResponse, }; use nym_api_requests::pagination::{PaginatedResponse, Pagination}; use nym_contracts_common::NaiveFloat; @@ -17,7 +17,8 @@ use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::NymNodeDetails; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use time::Date; +use std::time::Duration; +use time::{Date, OffsetDateTime}; use utoipa::{IntoParams, ToSchema}; pub(crate) mod legacy; @@ -25,6 +26,7 @@ pub(crate) mod unstable; pub(crate) fn nym_node_routes() -> Router { Router::new() + .route("/refresh-described", post(refresh_described)) .route("/noise", get(nodes_noise)) .route("/bonded", get(get_bonded_nodes)) .route("/described", get(get_described_nodes)) @@ -42,6 +44,63 @@ pub(crate) fn nym_node_routes() -> Router { .route("/uptime-history/:node_id", get(get_node_uptime_history)) } +#[utoipa::path( + tag = "Nym Nodes", + post, + request_body = NodeRefreshBody, + path = "/refresh-described", + context_path = "/v1/nym-nodes", +)] +async fn refresh_described( + State(state): State, + Json(request_body): Json, +) -> AxumResult> { + let Some(refresh_data) = state + .nym_contract_cache() + .get_node_refresh_data(request_body.node_identity) + .await + else { + return Err(AxumErrorResponse::not_found(format!( + "node with identity {} does not seem to exist", + request_body.node_identity + ))); + }; + + if !request_body.verify_signature() { + return Err(AxumErrorResponse::unauthorised("invalid request signature")); + } + + if request_body.is_stale() { + return Err(AxumErrorResponse::bad_request("the request is stale")); + } + + let node_id = refresh_data.node_id(); + if let Some(last) = state.forced_refresh.last_refreshed(node_id).await { + // max 1 refresh a minute + let minute_ago = OffsetDateTime::now_utc() - Duration::from_secs(60); + if last > minute_ago { + return Err(AxumErrorResponse::too_many( + "already refreshed node in the last minute", + )); + } + } + // to make sure you can't ddos the endpoint while a request is in progress + state.forced_refresh.set_last_refreshed(node_id).await; + + if let Some(updated_data) = refresh_data.try_refresh().await { + let Ok(mut describe_cache) = state.described_nodes_cache.write().await else { + return Err(AxumErrorResponse::service_unavailable()); + }; + describe_cache.get_mut().force_update(updated_data) + } else { + return Err(AxumErrorResponse::unprocessable_entity( + "failed to refresh node description", + )); + } + + Ok(Json(())) +} + #[utoipa::path( tag = "Nym Nodes", get, diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs index 706d2b342b..405f6022a3 100644 --- a/nym-api/src/support/caching/cache.rs +++ b/nym-api/src/support/caching/cache.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use std::time::Duration; use thiserror::Error; use time::OffsetDateTime; -use tokio::sync::{RwLock, RwLockReadGuard}; +use tokio::sync::{RwLock, RwLockMappedWriteGuard, RwLockReadGuard, RwLockWriteGuard}; #[derive(Debug, Error)] #[error("the cache item has not been initialised")] @@ -45,6 +45,13 @@ impl SharedCache { RwLockReadGuard::try_map(guard, |a| a.inner.as_ref()).map_err(|_| UninitialisedCache) } + pub(crate) async fn write( + &self, + ) -> Result>, UninitialisedCache> { + let guard = self.0.write().await; + RwLockWriteGuard::try_map(guard, |a| a.inner.as_mut()).map_err(|_| UninitialisedCache) + } + // ignores expiration data #[allow(dead_code)] pub(crate) async fn unchecked_get_inner( @@ -134,6 +141,10 @@ impl Cache { self.as_at = OffsetDateTime::now_utc() } + pub(crate) fn get_mut(&mut self) -> &mut T { + &mut self.value + } + #[allow(dead_code)] pub fn has_expired(&self, ttl: Duration, now: Option) -> bool { let now = now.unwrap_or(OffsetDateTime::now_utc()); diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 286757fbef..d185c28df9 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -188,6 +188,7 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result>; #[derive(Clone)] pub(crate) struct AppState { + pub(crate) forced_refresh: ForcedRefresh, pub(crate) nym_contract_cache: NymContractCache, pub(crate) node_status_cache: NodeStatusCache, pub(crate) circulating_supply_cache: CirculatingSupplyCache, @@ -79,6 +82,24 @@ pub(crate) struct AppState { pub(crate) node_info_cache: unstable::NodeInfoCache, } +#[derive(Clone, Default)] +pub(crate) struct ForcedRefresh { + pub(crate) refreshes: Arc>>, +} + +impl ForcedRefresh { + pub(crate) async fn last_refreshed(&self, node_id: NodeId) -> Option { + self.refreshes.read().await.get(&node_id).copied() + } + + pub(crate) async fn set_last_refreshed(&self, node_id: NodeId) { + self.refreshes + .write() + .await + .insert(node_id, OffsetDateTime::now_utc()); + } +} + impl AppState { pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { &self.nym_contract_cache diff --git a/nym-api/src/support/legacy_helpers.rs b/nym-api/src/support/legacy_helpers.rs new file mode 100644 index 0000000000..d5135c7643 --- /dev/null +++ b/nym-api/src/support/legacy_helpers.rs @@ -0,0 +1,85 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_api_requests::legacy::{LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::NymNodeData; +use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_crypto::aes::cipher::crypto_common::rand_core::OsRng; +use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges; +use nym_mixnet_contract_common::{ + Gateway, GatewayBond, LegacyMixLayer, MixNode, MixNodeBond, NymNodeDetails, +}; +use rand::prelude::SliceRandom; + +pub(crate) fn to_legacy_mixnode( + nym_node: &NymNodeDetails, + description: &NymNodeData, +) -> LegacyMixNodeDetailsWithLayer { + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + let mut rng = OsRng; + + // slap a random layer on it because legacy clients don't understand a concept of layerless mixnodes + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + let layer = layer_choices.choose(&mut rng).copied().unwrap(); + + LegacyMixNodeDetailsWithLayer { + bond_information: LegacyMixNodeBondWithLayer { + bond: MixNodeBond { + mix_id: nym_node.node_id(), + owner: nym_node.bond_information.owner.clone(), + original_pledge: nym_node.bond_information.original_pledge.clone(), + mix_node: MixNode { + host: nym_node.bond_information.node.host.clone(), + mix_port: description.mix_port(), + verloc_port: description.verloc_port(), + http_api_port: nym_node + .bond_information + .node + .custom_http_port + .unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT), + sphinx_key: description.host_information.keys.x25519.to_base58_string(), + identity_key: nym_node.bond_information.node.identity_key.clone(), + version: description.build_information.build_version.clone(), + }, + proxy: None, + bonding_height: nym_node.bond_information.bonding_height, + is_unbonding: nym_node.bond_information.is_unbonding, + }, + layer, + }, + rewarding_details: nym_node.rewarding_details.clone(), + pending_changes: LegacyPendingMixNodeChanges { + pledge_change: nym_node.pending_changes.pledge_change, + }, + } +} + +pub(crate) fn to_legacy_gateway( + nym_node: &NymNodeDetails, + description: &NymNodeData, +) -> GatewayBond { + GatewayBond { + pledge_amount: nym_node.bond_information.original_pledge.clone(), + owner: nym_node.bond_information.owner.clone(), + block_height: nym_node.bond_information.bonding_height, + gateway: Gateway { + host: nym_node.bond_information.node.host.clone(), + mix_port: description.mix_port(), + clients_port: description.mixnet_websockets.ws_port, + location: description + .auxiliary_details + .location + .map(|c| c.to_string()) + .unwrap_or_default(), + sphinx_key: description.host_information.keys.x25519.to_base58_string(), + identity_key: nym_node.bond_information.node.identity_key.clone(), + version: description.build_information.build_version.clone(), + }, + proxy: None, + } +} diff --git a/nym-api/src/support/mod.rs b/nym-api/src/support/mod.rs index 28abaf3b94..a1773f9804 100644 --- a/nym-api/src/support/mod.rs +++ b/nym-api/src/support/mod.rs @@ -7,3 +7,5 @@ pub(crate) mod config; pub(crate) mod http; pub(crate) mod nyxd; pub(crate) mod storage; + +pub(crate) mod legacy_helpers; diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index f41eabf508..8a2959aed7 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -983,7 +983,8 @@ impl StorageManager { since: i64, until: i64, ) -> Result, sqlx::Error> { - sqlx::query_as( + sqlx::query_as!( + ActiveGateway, r#" SELECT DISTINCT identity, node_id as "node_id: NodeId", id FROM gateway_details @@ -993,9 +994,9 @@ impl StorageManager { SELECT 1 FROM gateway_status WHERE timestamp > ? AND timestamp < ? ) "#, + since, + until ) - .bind(since) - .bind(until) .fetch_all(&self.connection_pool) .await } @@ -1328,7 +1329,7 @@ pub(crate) mod v3_migration { identity VARCHAR NOT NULL UNIQUE ); - INSERT INTO gateway_details_temp SELECT * FROM gateway_details; + INSERT INTO gateway_details_temp(id, node_id, identity) SELECT id, node_id, identity FROM gateway_details; DROP TABLE gateway_details; ALTER TABLE gateway_details_temp RENAME TO gateway_details; "#, diff --git a/nym-api/src/support/storage/models.rs b/nym-api/src/support/storage/models.rs index 6a7b120e66..1418cab9f8 100644 --- a/nym-api/src/support/storage/models.rs +++ b/nym-api/src/support/storage/models.rs @@ -87,7 +87,7 @@ pub struct GatewayDetails { impl From for TestNode { fn from(value: GatewayDetails) -> Self { TestNode { - node_id: None, + node_id: Some(value.node_id), identity_key: Some(value.identity), } } diff --git a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml index 462fa8a4c1..86e281eb8a 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml @@ -18,7 +18,7 @@ serde = { workspace = true, features = ["derive"] } serde_json.workspace = true time = { workspace = true, features = ["serde", "formatting", "parsing"] } tsify = { workspace = true, optional = true } -reqwest = { workspace = true, features = ["json"] } +reqwest = { workspace = true, features = ["json", "rustls-tls"] } wasm-bindgen = { workspace = true, optional = true } ## openapi: diff --git a/nym-node-status-agent/Cargo.toml b/nym-node-status-agent/Cargo.toml index f89065b5b3..f7d8543a1f 100644 --- a/nym-node-status-agent/Cargo.toml +++ b/nym-node-status-agent/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-node-status-agent" -version = "0.1.4" +version = "0.1.6" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-node-status-agent/Dockerfile b/nym-node-status-agent/Dockerfile index e39b58ce27..b50ce8be3d 100644 --- a/nym-node-status-agent/Dockerfile +++ b/nym-node-status-agent/Dockerfile @@ -37,4 +37,4 @@ COPY --from=builder /usr/src/nym/target/release/nym-node-status-agent ./ COPY --from=builder /usr/src/nym-vpn-client/nym-vpn-core/target/release/nym-gateway-probe ./ ENV NODE_STATUS_AGENT_PROBE_PATH=/nym/nym-gateway-probe -ENTRYPOINT [ "/nym/nym-node-status-agent", "run-probe" ] +ENTRYPOINT [ "/nym/nym-node-status-agent", "run-probe" ] \ No newline at end of file diff --git a/nym-node-status-agent/src/cli.rs b/nym-node-status-agent/src/cli.rs index 81e70e2da9..a7bbef5ebe 100644 --- a/nym-node-status-agent/src/cli.rs +++ b/nym-node-status-agent/src/cli.rs @@ -1,3 +1,4 @@ +use anyhow::bail; use clap::{Parser, Subcommand}; use nym_bin_common::bin_info; use nym_common_models::ns_api::TestrunAssignment; @@ -51,11 +52,13 @@ impl Args { let version = probe.version().await; tracing::info!("Probe version:\n{}", version); - let testrun = request_testrun(&server_address).await?; + if let Some(testrun) = request_testrun(&server_address).await? { + let log = probe.run_and_get_log(&Some(testrun.gateway_identity_key)); - let log = probe.run_and_get_log(&Some(testrun.gateway_identity_key)); - - submit_results(&server_address, testrun.testrun_id, log).await?; + submit_results(&server_address, testrun.testrun_id, log).await?; + } else { + tracing::info!("No testruns available, exiting") + } Ok(()) } @@ -64,16 +67,26 @@ impl Args { const URL_BASE: &str = "internal/testruns"; #[instrument(level = "debug", skip_all)] -async fn request_testrun(server_addr: &str) -> anyhow::Result { +async fn request_testrun(server_addr: &str) -> anyhow::Result> { let target_url = format!("{}/{}", server_addr, URL_BASE); let client = reqwest::Client::new(); - let res = client - .get(target_url) - .send() - .await - .and_then(|response| response.error_for_status())?; - res.json() - .await + let res = client.get(target_url).send().await?; + let status = res.status(); + let response_text = res.text().await?; + + if status.is_client_error() { + bail!("{}: {}", status, response_text); + } else if status.is_server_error() { + if matches!(status, reqwest::StatusCode::SERVICE_UNAVAILABLE) + && response_text.contains("No testruns available") + { + return Ok(None); + } else { + bail!("{}: {}", status, response_text); + } + } + + serde_json::from_str(&response_text) .map(|testrun| { tracing::info!("Received testrun assignment: {:?}", testrun); testrun diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml index 0969d31045..511ab91b59 100644 --- a/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "0.1.5" +version = "0.1.6" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/src/http/api/mod.rs index 4e4693ac1f..ed24fa80f5 100644 --- a/nym-node-status-api/src/http/api/mod.rs +++ b/nym-node-status-api/src/http/api/mod.rs @@ -1,10 +1,7 @@ use anyhow::anyhow; use axum::{response::Redirect, Router}; use tokio::net::ToSocketAddrs; -use tower_http::{ - cors::CorsLayer, - trace::{DefaultOnResponse, TraceLayer}, -}; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; @@ -61,10 +58,7 @@ impl RouterBuilder { // CORS layer needs to wrap other API layers .layer(setup_cors()) // logger should be outermost layer - .layer( - TraceLayer::new_for_http() - .on_response(DefaultOnResponse::new().level(tracing::Level::DEBUG)), - ) + .layer(TraceLayer::new_for_http()) } } diff --git a/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/src/http/api/testruns.rs index e55e462110..b13d62ba88 100644 --- a/nym-node-status-api/src/http/api/testruns.rs +++ b/nym-node-status-api/src/http/api/testruns.rs @@ -49,7 +49,8 @@ async fn request_testrun(State(state): State) -> HttpResult Err(HttpError::internal_with_logging(err)), diff --git a/nym-node-status-api/src/http/error.rs b/nym-node-status-api/src/http/error.rs index 808ace9cec..a7fe9d98ab 100644 --- a/nym-node-status-api/src/http/error.rs +++ b/nym-node-status-api/src/http/error.rs @@ -27,9 +27,9 @@ impl HttpError { } } - pub(crate) fn no_available_testruns() -> Self { + pub(crate) fn no_testruns_available() -> Self { Self { - message: serde_json::json!({"message": "No available testruns"}).to_string(), + message: serde_json::json!({"message": "No testruns available"}).to_string(), status: axum::http::StatusCode::SERVICE_UNAVAILABLE, } } diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index acd80a0777..0e112973b3 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.1.9" +version = "1.1.10" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -53,6 +53,7 @@ nym-sphinx-acknowledgements = { path = "../common/nymsphinx/acknowledgements" } nym-sphinx-addressing = { path = "../common/nymsphinx/addressing" } nym-task = { path = "../common/task" } nym-types = { path = "../common/types" } +nym-validator-client = { path = "../common/client-libs/validator-client" } nym-wireguard = { path = "../common/wireguard" } nym-wireguard-types = { path = "../common/wireguard-types", default-features = false } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index b7e97fb624..bebefa0aef 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -32,13 +32,18 @@ use nym_node_http_api::{NymNodeHTTPServer, NymNodeRouter}; use nym_sphinx_acknowledgements::AckKey; use nym_sphinx_addressing::Recipient; use nym_task::{TaskClient, TaskManager}; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::models::NodeRefreshBody; +use nym_validator_client::NymApiClient; use nym_wireguard::{peer_controller::PeerControlRequest, WireguardGatewayData}; use rand::rngs::OsRng; use rand::{CryptoRng, RngCore}; use std::path::Path; use std::sync::Arc; +use std::time::Duration; use tokio::sync::mpsc; -use tracing::{debug, error, info, trace}; +use tokio::time::timeout; +use tracing::{debug, error, info, trace, warn}; use zeroize::Zeroizing; use self::helpers::load_x25519_wireguard_keypair; @@ -750,6 +755,38 @@ impl NymNode { .await?) } + async fn try_refresh_remote_nym_api_cache(&self) { + info!("attempting to request described cache request from nym-api..."); + if self.config.mixnet.nym_api_urls.is_empty() { + warn!("no nym-api urls available"); + return; + } + + for nym_api in &self.config.mixnet.nym_api_urls { + info!("trying {nym_api}..."); + let client = NymApiClient::new_with_user_agent(nym_api.clone(), bin_info_owned!()); + + // make new request every time in case previous one takes longer and invalidates the signature + let request = NodeRefreshBody::new(self.ed25519_identity_keys.private_key()); + match timeout( + Duration::from_secs(10), + client.nym_api.force_refresh_describe_cache(&request), + ) + .await + { + Ok(Ok(_)) => { + info!("managed to refresh own self-described data cache") + } + Ok(Err(request_failure)) => { + warn!("failed to resolve the refresh request: {request_failure}") + } + Err(_timeout) => { + warn!("timed out while attempting to resolve the request. the cache might be stale") + } + }; + } + } + pub(crate) async fn run(self) -> Result<(), NymNodeError> { let mut task_manager = TaskManager::default().named("NymNode"); let http_server = self @@ -764,6 +801,8 @@ impl NymNode { } }); + self.try_refresh_remote_nym_api_cache().await; + match self.config.mode { NodeMode::Mixnode => { self.start_mixnode(task_manager.subscribe_named("mixnode"))?; diff --git a/nym-wallet/CHANGELOG.md b/nym-wallet/CHANGELOG.md index 146e6174ed..aba29abef0 100644 --- a/nym-wallet/CHANGELOG.md +++ b/nym-wallet/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] +## [2024.13-magura] (2024-11-18) + +- bugfix: [wallet] displaying delegations for native nymnodes ([#5087]) +- bugfix: wallet backend fixes ([#5070]) +- Feature/wallet bonding fixes ([#5064]) + +[#5087]: https://github.com/nymtech/nym/pull/5087 +[#5070]: https://github.com/nymtech/nym/pull/5070 +[#5064]: https://github.com/nymtech/nym/pull/5064 + ## [v1.2.13] (2024-05-08) - Bug fix: wallet delegations list is empty when RPC node doesn't hold block ([#4565]) diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index f41ae89117..81fa6e2a7b 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3535,7 +3535,7 @@ dependencies = [ [[package]] name = "nym_wallet" -version = "1.2.14" +version = "1.2.15" dependencies = [ "async-trait", "base64 0.13.1", diff --git a/nym-wallet/package.json b/nym-wallet/package.json index 3d97eefd26..341da85fb1 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/nym-wallet-app", - "version": "1.2.14", + "version": "1.2.15", "license": "MIT", "main": "index.js", "scripts": { diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 88032e5efb..fcf0f6417e 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym_wallet" -version = "1.2.14" +version = "1.2.15" description = "Nym Native Wallet" authors = ["Nym Technologies SA"] license = "" diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 6d960031fe..744aeb1bb7 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -7,7 +7,9 @@ use crate::vesting::delegate::vesting_undelegate_from_mixnode; use nym_mixnet_contract_common::mixnode::MixStakeSaturationResponse; use nym_mixnet_contract_common::NodeId; use nym_types::currency::DecCoin; -use nym_types::delegation::{Delegation, DelegationWithEverything, DelegationsSummaryResponse}; +use nym_types::delegation::{ + Delegation, DelegationWithEverything, DelegationsSummaryResponse, NodeInformation, +}; use nym_types::deprecated::{ convert_to_delegation_events, DelegationEvent, WrappedDelegationEvent, }; @@ -19,6 +21,7 @@ use nym_validator_client::nyxd::contract_traits::{ MixnetQueryClient, MixnetSigningClient, NymContractsProvider, PagedMixnetQueryClient, }; use nym_validator_client::nyxd::Fee; +use nym_validator_client::DirectSigningHttpRpcValidatorClient; use tap::TapFallible; #[tauri::command] @@ -141,6 +144,58 @@ pub async fn undelegate_all_from_mixnode( Ok(res) } +pub(crate) async fn get_node_information( + client: &DirectSigningHttpRpcValidatorClient, + node_id: NodeId, + error_strings: &mut Vec, +) -> Result, BackendError> { + let native_nymnode = client + .nyxd + .get_nymnode_details(node_id) + .await + .inspect_err(|err| { + let str_err = + format!("Failed to get nymnode details for node_id = {node_id}. Error: {err}"); + log::error!(" <<< {str_err}",); + error_strings.push(str_err); + })?; + + if let Some(native) = native_nymnode.details { + return Ok(Some(NodeInformation { + is_unbonding: native.is_unbonding(), + owner: native.bond_information.owner.to_string(), + mix_id: node_id, + node_identity: native.bond_information.node.identity_key, + rewarding_details: native.rewarding_details, + })); + } + + let legacy_mixnode = client + .nyxd + .get_mixnode_details(node_id) + .await + .inspect_err(|err| { + let str_err = format!( + "Failed to get legacy mixnode details for node_id = {node_id}. Error: {err}", + ); + log::error!(" <<< {}", str_err); + error_strings.push(str_err); + })? + .mixnode_details; + + if let Some(legacy) = legacy_mixnode { + return Ok(Some(NodeInformation { + is_unbonding: legacy.is_unbonding(), + owner: legacy.bond_information.owner.to_string(), + mix_id: node_id, + node_identity: legacy.bond_information.mix_node.identity_key, + rewarding_details: legacy.rewarding_details, + })); + } + + Ok(None) +} + // TODO: fix later (yeah...) #[allow(deprecated)] #[tauri::command] @@ -208,21 +263,9 @@ pub async fn get_all_mix_delegations( d.amount ); - let mixnode = client - .nyxd - .get_mixnode_details(d.mix_id) - .await - .tap_err(|err| { - let str_err = format!( - "Failed to get mixnode details for mix_id = {}. Error: {}", - d.mix_id, err - ); - log::error!(" <<< {}", str_err); - error_strings.push(str_err); - })? - .mixnode_details; + let node_details = get_node_information(client, d.mix_id, &mut error_strings).await?; - let accumulated_by_operator = mixnode + let accumulated_by_operator = node_details .as_ref() .map(|m| { guard.display_coin_from_base_decimal(&base_mix_denom, m.rewarding_details.operator) @@ -238,10 +281,13 @@ pub async fn get_all_mix_delegations( }) .unwrap_or_default(); - let accumulated_by_delegates = mixnode + let accumulated_by_delegates = node_details .as_ref() .map(|m| { - guard.display_coin_from_base_decimal(&base_mix_denom, m.rewarding_details.delegates) + reg.attempt_create_display_coin_from_base_dec_amount( + &base_mix_denom, + m.rewarding_details.delegates, + ) }) .transpose() .tap_err(|err| { @@ -254,7 +300,7 @@ pub async fn get_all_mix_delegations( }) .unwrap_or_default(); - let cost_params = mixnode + let cost_params = node_details .as_ref() .map(|m| { NodeCostParams::from_mixnet_contract_mixnode_cost_params( @@ -335,21 +381,26 @@ pub async fn get_all_mix_delegations( " >>> Get average uptime percentage: mix_iid = {}", d.mix_id ); - let avg_uptime_percent = client + + let current_performance = client .nym_api - .get_mixnode_avg_uptime(d.mix_id) + .get_current_node_performance(d.mix_id) .await - .tap_err(|err| { + .inspect_err(|err| { let str_err = format!( - "Failed to get average uptime percentage for mix_id = {}. Error: {}", - d.mix_id, err + "Failed to get current node performance for node_id = {}. Error: {err}", + d.mix_id ); log::error!(" <<< {}", str_err); error_strings.push(str_err); }) .ok() - .map(|r| r.avg_uptime); - log::trace!(" <<< {:?}", avg_uptime_percent); + .and_then(|r| r.performance); + + // convert to old u8 + let current_uptime = current_performance.map(|p| (p * 100.) as u8); + + log::trace!(" <<< {:?}", current_uptime); log::trace!( " >>> Convert delegated on block height to timestamp: block_height = {}", @@ -377,9 +428,9 @@ pub async fn get_all_mix_delegations( pending_events.len() ); - let mixnode_is_unbonding = mixnode.as_ref().map(|m| m.is_unbonding()); + let mixnode_is_unbonding = node_details.as_ref().map(|m| m.is_unbonding); log::trace!( - " >>> mixnode with mix_id: {} is unbonding: {:?}", + " >>> node with mix_id: {} is unbonding: {:?}", d.mix_id, mixnode_is_unbonding ); @@ -387,16 +438,14 @@ pub async fn get_all_mix_delegations( with_everything.push(DelegationWithEverything { owner: d.owner, mix_id: d.mix_id, - node_identity: mixnode - .map(|m| m.bond_information.mix_node.identity_key) - .unwrap_or_default(), + node_identity: node_details.map(|m| m.node_identity).unwrap_or_default(), amount: d.amount, block_height: d.height, uses_vesting_contract_tokens, delegated_on_iso_datetime, stake_saturation: stake_saturation.uncapped_saturation, accumulated_by_operator, - avg_uptime_percent, + avg_uptime_percent: current_uptime, accumulated_by_delegates, cost_params, unclaimed_rewards: accumulated_rewards, diff --git a/nym-wallet/src-tauri/tauri.conf.json b/nym-wallet/src-tauri/tauri.conf.json index 0067868c37..e493efdfed 100644 --- a/nym-wallet/src-tauri/tauri.conf.json +++ b/nym-wallet/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "nym-wallet", - "version": "1.2.14" + "version": "1.2.15" }, "build": { "distDir": "../dist", diff --git a/nym-wallet/src/components/Bonding/BondedGateway.tsx b/nym-wallet/src/components/Bonding/BondedGateway.tsx index ab8ee84209..53d0a06b16 100644 --- a/nym-wallet/src/components/Bonding/BondedGateway.tsx +++ b/nym-wallet/src/components/Bonding/BondedGateway.tsx @@ -113,6 +113,7 @@ export const BondedGateway = ({ startIcon={} variant="contained" disableElevation + disabled={!!gateway.proxy} onClick={onShowMigrateToNymNodeModal} > Migrate to Nym Node diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index 5d0fc68e64..6b2977b48e 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -190,15 +190,21 @@ export const BondedMixnode = ({ - + + + + {nextEpoch instanceof Error ? null : ( diff --git a/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx b/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx index 8e00ddbfa5..0243e765f3 100644 --- a/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx @@ -4,11 +4,10 @@ import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { ModalFee } from 'src/components/Modals/ModalFee'; import { useGetFee } from 'src/hooks/useGetFee'; -import { simulateClaimOperatorReward, simulateVestingClaimOperatorReward } from 'src/requests'; import { AppContext } from 'src/context'; import { BalanceWarning } from 'src/components/FeeWarning'; import { Box } from '@mui/material'; -import { TBondedMixnode } from 'src/requests/mixnodeDetails'; +import { TBondedNymNode } from 'src/requests/nymNodeDetails'; export const RedeemRewardsModal = ({ node, @@ -16,23 +15,18 @@ export const RedeemRewardsModal = ({ onError, onClose, }: { - node: TBondedMixnode; + node: TBondedNymNode; onConfirm: (fee?: FeeDetails) => Promise; onError: (err: string) => void; onClose: () => void; }) => { - const { fee, getFee, isFeeLoading, feeError } = useGetFee(); + const { fee, isFeeLoading, feeError } = useGetFee(); const { userBalance } = useContext(AppContext); useEffect(() => { if (feeError) onError(feeError); }, [feeError]); - useEffect(() => { - if (node.proxy) getFee(simulateVestingClaimOperatorReward, {}); - else getFee(simulateClaimOperatorReward, {}); - }, []); - const handleOnOK = async () => onConfirm(fee); return ( diff --git a/nym-wallet/src/context/bonding.tsx b/nym-wallet/src/context/bonding.tsx index 19652b6cff..6ff1ca3393 100644 --- a/nym-wallet/src/context/bonding.tsx +++ b/nym-wallet/src/context/bonding.tsx @@ -136,13 +136,14 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen if (bondedNode && isNymNode(bondedNode)) tx = await unbondNymNodeRequest(fee?.fee); if (bondedNode && isMixnode(bondedNode) && !bondedNode.proxy) tx = await unbondMixnodeRequest(fee?.fee); if (bondedNode && isGateway(bondedNode) && !bondedNode.proxy) tx = await unbondGatewayRequest(fee?.fee); + return tx; } catch (e) { Console.warn(e); setError(`an error occurred: ${e as string}`); } finally { setIsLoading(false); } - return tx; + return undefined; }; const updateNymNodeConfig = async (data: NodeConfigUpdate) => { @@ -153,13 +154,14 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen if (clientDetails?.client_address) { await getNodeDetails(clientDetails?.client_address); } + return tx; } catch (e) { Console.warn(e); setError(`an error occurred: ${e}`); } finally { setIsLoading(false); } - return tx; + return undefined; }; const redeemRewards = async (fee?: FeeDetails) => { @@ -168,12 +170,13 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen try { if (bondedNode && !isNymNode(bondedNode)) tx = await vestingClaimOperatorReward(fee?.fee); else tx = await claimOperatorReward(fee?.fee); + return tx; } catch (e: any) { setError(`an error occurred: ${e}`); } finally { setIsLoading(false); } - return tx; + return undefined; }; const updateBondAmount = async (data: TUpdateBondArgs) => { @@ -217,24 +220,35 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen const migrateVestedMixnode = async () => { setIsLoading(true); - const tx = await tauriMigrateVestedMixnode(); - setIsLoading(false); - return tx; + try { + const tx = await tauriMigrateVestedMixnode(); + setIsLoading(false); + return tx; + } catch (e) { + Console.error(e); + setError(`an error occurred: ${e}`); + } + return undefined; }; const migrateLegacyNode = async () => { setIsLoading(true); - let tx: TransactionExecuteResult | undefined; + try { + let tx: TransactionExecuteResult | undefined; - if (bondedNode && isMixnode(bondedNode)) { - tx = await migrateLegacyMixnodeReq(); + if (bondedNode && isMixnode(bondedNode)) { + tx = await migrateLegacyMixnodeReq(); + } + if (bondedNode && isGateway(bondedNode)) { + tx = await migrateLegacyGatewayReq(); + } + return tx; + } catch (e) { + Console.error(e); + setError(`an error occurred: ${e}`); } - if (bondedNode && isGateway(bondedNode)) { - tx = await migrateLegacyGatewayReq(); - } - setIsLoading(false); - return tx; + return undefined; }; const memoizedValue = useMemo( diff --git a/nym-wallet/src/pages/bonding/Bonding.tsx b/nym-wallet/src/pages/bonding/Bonding.tsx index b66ffbe58e..57dc05bbe4 100644 --- a/nym-wallet/src/pages/bonding/Bonding.tsx +++ b/nym-wallet/src/pages/bonding/Bonding.tsx @@ -53,7 +53,7 @@ export const Bonding = () => { if (!bondedNode) { return false; } - if (isMixnode(bondedNode) && !bondedNode.isUnbonding) { + if (isMixnode(bondedNode) && !bondedNode.isUnbonding && !bondedNode.proxy) { return true; } if (isGateway(bondedNode)) { @@ -197,7 +197,14 @@ export const Bonding = () => { }; if (error) { - return refresh()} />; + return ( + refresh()} + /> + ); } return ( @@ -292,7 +299,7 @@ export const Bonding = () => { /> )} - {showModal === 'redeem' && bondedNode && isMixnode(bondedNode) && ( + {showModal === 'redeem' && bondedNode && isNymNode(bondedNode) && ( setShowModal(undefined)} diff --git a/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx index dd86326130..09e11d37d5 100644 --- a/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx @@ -14,12 +14,9 @@ import { useBondingContext, BondingContextProvider } from 'src/context'; import { AppContext, urls } from 'src/context/main'; import { getIntervalAsDate } from 'src/utils'; -import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { NodeGeneralSettings } from './settings-pages/general-settings'; import { NodeUnbondPage } from './settings-pages/NodeUnbondPage'; import { NavItems, makeNavItems } from './node-settings.constant'; -import { ApyPlayground } from './apy-playground'; -import { NodeTestPage } from './node-test'; export const NodeSettings = () => { const theme = useTheme(); @@ -39,9 +36,9 @@ export const NodeSettings = () => { if (location.state === 'unbond') { setValue('Unbond'); } - if (location.state === 'test-node') { - setValue('Test my node'); - } + // if (location.state === 'test-node') { + // setValue('Test my node'); + // } }, [location]); const handleUnbond = async (fee?: FeeDetails) => { @@ -129,11 +126,11 @@ export const NodeSettings = () => { > {value === 'General' && bondedNode && } - {value === 'Test my node' && } + {/* {value === 'Test my node' && } */} {value === 'Unbond' && bondedNode && ( )} - {value === 'Playground' && bondedNode && } + {/* {value === 'Playground' && bondedNode && } */} {confirmationDetails && confirmationDetails.status === 'success' && ( { const navItems: NavItems[] = ['Unbond']; if (isNymNode(bondedNode)) { - // add these items to the beginning of the array "General", "Test my node", "Playground" - navItems.unshift('General', 'Test my node', 'Playground'); + // Add these items to the beginning of the array "General", "Test my node", "Playground" + // Temporarily removed , 'Test my node due to wasm issues which we need to fix + // 'Playground' due to freezing issues + navItems.unshift('General'); } return navItems; }; -export type NavItems = 'General' | 'Unbond' | 'Test my node' | 'Playground'; +// And these back in once fixed. +// 'Playground' | 'Test my node' include in array at a later point +export type NavItems = 'General' | 'Unbond'; diff --git a/nym-wallet/src/requests/actions.ts b/nym-wallet/src/requests/actions.ts index 6907cf054a..835fcb465b 100644 --- a/nym-wallet/src/requests/actions.ts +++ b/nym-wallet/src/requests/actions.ts @@ -7,7 +7,7 @@ import { NodeConfigUpdate, GatewayConfigUpdate, } from '@nymproject/types'; -import { TBondGatewayArgs, TBondGatewaySignatureArgs, TNodeConfigUpdateArgs } from '../types'; +import { TBondGatewayArgs, TBondGatewaySignatureArgs } from '../types'; import { invokeWrapper } from './wrapper'; export const bondGateway = async (args: TBondGatewayArgs) => diff --git a/nym-wallet/src/requests/rewards.ts b/nym-wallet/src/requests/rewards.ts index 36b90838e7..ed5dcb797a 100644 --- a/nym-wallet/src/requests/rewards.ts +++ b/nym-wallet/src/requests/rewards.ts @@ -6,7 +6,7 @@ export const claimOperatorReward = async (fee?: Fee) => export const claimDelegatorRewards = async (mixId: number, fee?: FeeDetails) => invokeWrapper('claim_locked_and_unlocked_delegator_reward', { - mixId, + nodeId: mixId, fee: fee?.fee, }); diff --git a/service-providers/authenticator/src/error.rs b/service-providers/authenticator/src/error.rs index 07713eb3c3..2f272e4d16 100644 --- a/service-providers/authenticator/src/error.rs +++ b/service-providers/authenticator/src/error.rs @@ -89,6 +89,9 @@ pub enum AuthenticatorError { #[error("storage should have the requested bandwidht entry")] MissingClientBandwidthEntry, + #[error("unknown version number")] + UnknownVersion, + #[error("{0}")] PublicKey(#[from] nym_wireguard_types::Error), diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index e4c68eb484..8fbd3e2b29 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -6,18 +6,17 @@ use std::{ time::{Duration, SystemTime}, }; +use crate::{config::Config, error::*}; use crate::{error::AuthenticatorError, peer_manager::PeerManager}; use defguard_wireguard_rs::net::IpAddrMask; use defguard_wireguard_rs::{host::Peer, key::Key}; use futures::StreamExt; +use nym_authenticator_requests::latest::registration::RegistrationData; use nym_authenticator_requests::{ - latest::{ - registration::{ - FinalMessage, GatewayClient, InitMessage, IpPair, PendingRegistrations, PrivateIPs, - RegistrationData, RegistredData, RemainingBandwidthData, - }, - request::{AuthenticatorRequest, AuthenticatorRequestData}, - response::AuthenticatorResponse, + latest::registration::{GatewayClient, PendingRegistrations, PrivateIPs}, + traits::{ + AuthenticatorRequest, AuthenticatorVersion, FinalMessage, InitMessage, + QueryBandwidthMessage, TopUpMessage, }, v1, v2, v3, v4, CURRENT_VERSION, }; @@ -30,7 +29,7 @@ use nym_crypto::asymmetric::x25519::KeyPair; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::Storage; use nym_sdk::mixnet::{InputMessage, MixnetMessageSender, Recipient, TransmissionLane}; -use nym_service_provider_requests_common::ServiceProviderType; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::TaskHandle; use nym_wireguard::WireguardGatewayData; @@ -39,9 +38,7 @@ use rand::{prelude::IteratorRandom, thread_rng}; use tokio::sync::RwLock; use tokio_stream::wrappers::IntervalStream; -use crate::{config::Config, error::*}; - -type AuthenticatorHandleResult = Result; +type AuthenticatorHandleResult = Result<(Vec, Recipient)>; const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute pub(crate) struct RegistredAndFree { @@ -151,22 +148,87 @@ impl MixnetListener { async fn on_initial_request( &mut self, - init_message: InitMessage, + init_message: Box, + protocol: Protocol, request_id: u64, reply_to: Recipient, ) -> AuthenticatorHandleResult { - let remote_public = init_message.pub_key; + let remote_public = init_message.pub_key(); let nonce: u64 = fastrand::u64(..); let mut registred_and_free = self.registred_and_free.write().await; if let Some(registration_data) = registred_and_free .registration_in_progres .get(&remote_public) { - return Ok(AuthenticatorResponse::new_pending_registration_success( - registration_data.clone(), - request_id, - reply_to, - )); + let gateway_data = registration_data.gateway_data.clone(); + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V1 => { + v1::response::AuthenticatorResponse::new_pending_registration_success( + v1::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: v1::GatewayClient { + pub_key: gateway_data.pub_key, + private_ip: gateway_data.private_ips.ipv4.into(), + mac: v1::ClientMac::new(gateway_data.mac.to_vec()), + }, + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V2 => { + v2::response::AuthenticatorResponse::new_pending_registration_success( + v2::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.clone().into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V3 => { + v3::response::AuthenticatorResponse::new_pending_registration_success( + v3::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.clone().into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V4 => { + v4::response::AuthenticatorResponse::new_pending_registration_success( + v4::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.clone(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), + }; + return Ok((bytes, reply_to)); } let peer = self.peer_manager.query_peer(remote_public).await?; @@ -190,15 +252,62 @@ impl MixnetListener { "there should be one private IP pair in the list".to_string(), )); }; - return Ok(AuthenticatorResponse::new_registered( - RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), - private_ips: IpPair::new(allowed_ipv4, allowed_ipv6), - wg_port: self.config.authenticator.announced_port, - }, - reply_to, - request_id, - )); + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered( + v1::registration::RegistredData { + pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + private_ip: allowed_ipv4.into(), + wg_port: self.config.authenticator.announced_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })?, + AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered( + v2::registration::RegistredData { + pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + private_ip: allowed_ipv4.into(), + wg_port: self.config.authenticator.announced_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })?, + AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered( + v3::registration::RegistredData { + pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + private_ip: allowed_ipv4.into(), + wg_port: self.config.authenticator.announced_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })?, + AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered( + v4::registration::RegistredData { + pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + private_ips: (allowed_ipv4, allowed_ipv6).into(), + wg_port: self.config.authenticator.announced_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })?, + AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), + }; + return Ok((bytes, reply_to)); } let private_ip_ref = registred_and_free @@ -217,57 +326,117 @@ impl MixnetListener { ); let registration_data = RegistrationData { nonce, - gateway_data, + gateway_data: gateway_data.clone(), wg_port: self.config.authenticator.announced_port, }; registred_and_free .registration_in_progres .insert(remote_public, registration_data.clone()); + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V1 => { + v1::response::AuthenticatorResponse::new_pending_registration_success( + v1::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: v1::GatewayClient { + pub_key: gateway_data.pub_key, + private_ip: gateway_data.private_ips.ipv4.into(), + mac: v1::ClientMac::new(gateway_data.mac.to_vec()), + }, + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V2 => { + v2::response::AuthenticatorResponse::new_pending_registration_success( + v2::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V3 => { + v3::response::AuthenticatorResponse::new_pending_registration_success( + v3::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.into(), + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V4 => { + v4::response::AuthenticatorResponse::new_pending_registration_success( + v4::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data, + wg_port: registration_data.wg_port, + }, + request_id, + reply_to, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), + }; - Ok(AuthenticatorResponse::new_pending_registration_success( - registration_data, - request_id, - reply_to, - )) + Ok((bytes, reply_to)) } async fn on_final_request( &mut self, - final_message: FinalMessage, + final_message: Box, + protocol: Protocol, request_id: u64, reply_to: Recipient, ) -> AuthenticatorHandleResult { let mut registred_and_free = self.registred_and_free.write().await; let registration_data = registred_and_free .registration_in_progres - .get(&final_message.gateway_client.pub_key()) + .get(&final_message.pub_key()) .ok_or(AuthenticatorError::RegistrationNotInProgress)? .clone(); if final_message - .gateway_client .verify(self.keypair().private_key(), registration_data.nonce) .is_err() { return Err(AuthenticatorError::MacVerificationFailure); } - let mut peer = Peer::new(Key::new(final_message.gateway_client.pub_key.to_bytes())); + let mut peer = Peer::new(Key::new(final_message.pub_key().to_bytes())); + peer.allowed_ips + .push(IpAddrMask::new(final_message.private_ips().ipv4.into(), 32)); peer.allowed_ips.push(IpAddrMask::new( - final_message.gateway_client.private_ips.ipv4.into(), - 32, - )); - peer.allowed_ips.push(IpAddrMask::new( - final_message.gateway_client.private_ips.ipv6.into(), + final_message.private_ips().ipv6.into(), 128, )); // If gateway does ecash verification and client sends a credential, we do the additional // credential verification. Later this will become mandatory. - if let (Some(ecash_verifier), Some(credential)) = ( - self.ecash_verifier.clone(), - final_message.credential.clone(), - ) { + if let (Some(ecash_verifier), Some(credential)) = + (self.ecash_verifier.clone(), final_message.credential()) + { let client_id = ecash_verifier .storage() .insert_wireguard_peer(&peer, true) @@ -297,17 +466,56 @@ impl MixnetListener { } registred_and_free .registration_in_progres - .remove(&final_message.gateway_client.pub_key()); + .remove(&final_message.pub_key()); - Ok(AuthenticatorResponse::new_registered( - RegistredData { - pub_key: registration_data.gateway_data.pub_key, - private_ips: registration_data.gateway_data.private_ips, - wg_port: registration_data.wg_port, - }, - reply_to, - request_id, - )) + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered( + v1::registration::RegistredData { + pub_key: registration_data.gateway_data.pub_key, + private_ip: registration_data.gateway_data.private_ips.ipv4.into(), + wg_port: registration_data.wg_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered( + v2::registration::RegistredData { + pub_key: registration_data.gateway_data.pub_key, + private_ip: registration_data.gateway_data.private_ips.ipv4.into(), + wg_port: registration_data.wg_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered( + v3::registration::RegistredData { + pub_key: registration_data.gateway_data.pub_key, + private_ip: registration_data.gateway_data.private_ips.ipv4.into(), + wg_port: registration_data.wg_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered( + v4::registration::RegistredData { + pub_key: registration_data.gateway_data.pub_key, + private_ips: registration_data.gateway_data.private_ips, + wg_port: registration_data.wg_port, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), + }; + Ok((bytes, reply_to)) } async fn credential_verification( @@ -343,22 +551,75 @@ impl MixnetListener { async fn on_query_bandwidth_request( &mut self, - peer_public_key: PeerPublicKey, + msg: Box, + protocol: Protocol, request_id: u64, reply_to: Recipient, ) -> AuthenticatorHandleResult { - let bandwidth_data = self.peer_manager.query_bandwidth(peer_public_key).await?; - Ok(AuthenticatorResponse::new_remaining_bandwidth( - bandwidth_data, - reply_to, - request_id, - )) + let bandwidth_data = self.peer_manager.query_bandwidth(msg).await?; + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V1 => { + v1::response::AuthenticatorResponse::new_remaining_bandwidth( + bandwidth_data.map(|data| v1::registration::RemainingBandwidthData { + available_bandwidth: data.available_bandwidth as u64, + suspended: false, + }), + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V2 => { + v2::response::AuthenticatorResponse::new_remaining_bandwidth( + bandwidth_data.map(|data| v2::registration::RemainingBandwidthData { + available_bandwidth: data.available_bandwidth, + }), + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V3 => { + v3::response::AuthenticatorResponse::new_remaining_bandwidth( + bandwidth_data.map(|data| v3::registration::RemainingBandwidthData { + available_bandwidth: data.available_bandwidth, + }), + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::V4 => { + v4::response::AuthenticatorResponse::new_remaining_bandwidth( + bandwidth_data.map(|data| v4::registration::RemainingBandwidthData { + available_bandwidth: data.available_bandwidth, + }), + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| { + AuthenticatorError::FailedToSerializeResponsePacket { source: err } + })? + } + AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), + }; + Ok((bytes, reply_to)) } async fn on_topup_bandwidth_request( &mut self, - peer_public_key: PeerPublicKey, - credential: CredentialSpendingData, + msg: Box, + protocol: Protocol, request_id: u64, reply_to: Recipient, ) -> AuthenticatorHandleResult { @@ -367,7 +628,7 @@ impl MixnetListener { }; let client_id = ecash_verifier .storage() - .get_wireguard_peer(&peer_public_key.to_string()) + .get_wireguard_peer(&msg.pub_key().to_string()) .await? .ok_or(AuthenticatorError::MissingClientBandwidthEntry)? .client_id @@ -382,7 +643,7 @@ impl MixnetListener { let client_bandwidth = ClientBandwidth::new(bandwidth.into()); let mut verifier = CredentialVerifier::new( - CredentialSpendingRequest::new(credential), + CredentialSpendingRequest::new(msg.credential()), ecash_verifier.clone(), BandwidthStorageManager::new( ecash_verifier.storage().clone(), @@ -394,13 +655,31 @@ impl MixnetListener { ); let available_bandwidth = verifier.verify().await?; - Ok(AuthenticatorResponse::new_topup_bandwidth( - RemainingBandwidthData { - available_bandwidth, - }, - reply_to, - request_id, - )) + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_topup_bandwidth( + v4::registration::RemainingBandwidthData { + available_bandwidth, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_topup_bandwidth( + v3::registration::RemainingBandwidthData { + available_bandwidth, + }, + reply_to, + request_id, + ) + .to_bytes() + .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { + return Err(AuthenticatorError::UnknownVersion) + } + }; + + Ok((bytes, reply_to)) } async fn on_reconstructed_message( @@ -412,62 +691,52 @@ impl MixnetListener { reconstructed.sender_tag ); - let request = match deserialize_request(&reconstructed) { - Err(AuthenticatorError::InvalidPacketVersion(version)) => { - return self.on_version_mismatch(version, &reconstructed); - } - req => req, - }?; + let request = deserialize_request(&reconstructed)?; - match request.data { - AuthenticatorRequestData::Initial(init_msg) => { - self.on_initial_request(init_msg, request.request_id, request.reply_to) + match request { + AuthenticatorRequest::Initial { + msg, + reply_to, + request_id, + protocol, + } => { + self.on_initial_request(msg, protocol, request_id, reply_to) .await } - AuthenticatorRequestData::Final(final_msg) => { - self.on_final_request(*final_msg, request.request_id, request.reply_to) + AuthenticatorRequest::Final { + msg, + reply_to, + request_id, + protocol, + } => { + self.on_final_request(msg, protocol, request_id, reply_to) .await } - AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { - self.on_query_bandwidth_request( - peer_public_key, - request.request_id, - request.reply_to, - ) - .await + AuthenticatorRequest::QueryBandwidth { + msg, + reply_to, + request_id, + protocol, + } => { + self.on_query_bandwidth_request(msg, protocol, request_id, reply_to) + .await } - AuthenticatorRequestData::TopUpBandwidth(topup_message) => { - self.on_topup_bandwidth_request( - topup_message.pub_key, - topup_message.credential, - request.request_id, - request.reply_to, - ) - .await + AuthenticatorRequest::TopUpBandwidth { + msg, + reply_to, + request_id, + protocol, + } => { + self.on_topup_bandwidth_request(msg, protocol, request_id, reply_to) + .await } } } - fn on_version_mismatch( - &self, - version: u8, - _reconstructed: &ReconstructedMessage, - ) -> AuthenticatorHandleResult { - // If it's possible to parse, do so and return back a response, otherwise just drop - Err(AuthenticatorError::InvalidPacketVersion(version)) - } - // When an incoming mixnet message triggers a response that we send back. - async fn handle_response(&self, response: AuthenticatorResponse) -> Result<()> { - let recipient = response.recipient(); - - let response_packet = response.to_bytes().map_err(|err| { - log::error!("Failed to serialize response packet"); - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?; - + async fn handle_response(&self, response: Vec, recipient: Recipient) -> Result<()> { let input_message = - InputMessage::new_regular(recipient, response_packet, TransmissionLane::General, None); + InputMessage::new_regular(recipient, response, TransmissionLane::General, None); self.mixnet_client .send(input_message) .await @@ -491,8 +760,8 @@ impl MixnetListener { msg = self.mixnet_client.next() => { if let Some(msg) = msg { match self.on_reconstructed_message(msg).await { - Ok(response) => { - if let Err(err) = self.handle_response(response).await { + Ok((response, recipient)) => { + if let Err(err) = self.handle_response(response, recipient).await { log::error!("Mixnet listener failed to handle response: {err}"); } } @@ -524,8 +793,6 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result v1::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) - .map(Into::::into) - .map(Into::::into) .map(Into::into), [2, request_type] => { if request_type == ServiceProviderType::Authenticator as u8 { @@ -556,6 +823,7 @@ fn deserialize_request(reconstructed: &ReconstructedMessage) -> Result, ) -> Result> { - let key = Key::new(peer_public_key.to_bytes()); + let key = Key::new(msg.pub_key().to_bytes()); let (response_tx, response_rx) = oneshot::channel(); let msg = PeerControlRequest::QueryBandwidth { key, response_tx }; self.wireguard_gateway_data diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 6678d72c09..9ab6b58afd 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.43" +version = "1.1.44" authors.workspace = true edition.workspace = true rust-version = "1.70" diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 8451a20d89..2a4bdf0186 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.43" +version = "1.1.44" authors.workspace = true edition = "2021" license.workspace = true diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index fae37c502c..a1f0590c8d 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nymvisor" -version = "0.1.8" +version = "0.1.9" authors.workspace = true repository.workspace = true homepage.workspace = true