diff --git a/Cargo.lock b/Cargo.lock index c2eadc94b5..1dcf4528bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1909,6 +1909,7 @@ dependencies = [ "maxminddb", "nym-bin-common", "nym-contracts-common", + "nym-explorer-api-requests", "nym-mixnet-contract-common", "nym-network-defaults", "nym-task", @@ -3782,6 +3783,7 @@ dependencies = [ "nym-config", "nym-credential-storage", "nym-crypto", + "nym-explorer-api-requests", "nym-gateway-client", "nym-gateway-requests", "nym-network-defaults", @@ -3792,6 +3794,7 @@ dependencies = [ "nym-topology", "nym-validator-client", "rand 0.7.3", + "reqwest", "serde", "serde_json", "sha2 0.10.6", @@ -4004,6 +4007,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "nym-explorer-api-requests" +version = "0.1.0" +dependencies = [ + "nym-contracts-common", + "nym-mixnet-contract-common", + "nym-validator-client", + "schemars", + "serde", + "ts-rs", +] + [[package]] name = "nym-gateway" version = "1.1.23" @@ -4520,7 +4535,9 @@ dependencies = [ "nym-bandwidth-controller", "nym-client-core", "nym-config", + "nym-contracts-common", "nym-credential-storage", + "nym-mixnet-contract-common", "nym-network-defaults", "nym-service-providers-common", "nym-socks5-proxy-helpers", @@ -4530,10 +4547,13 @@ dependencies = [ "nym-validator-client", "pin-project", "rand 0.7.3", + "reqwest", + "schemars", "serde", "tap", "thiserror", "tokio", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 666d1f76fc..198f37890e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ members = [ "common/types", "common/wasm-utils", "explorer-api", + "explorer-api/explorer-api-requests", "gateway", "gateway/gateway-requests", "integrations/bity", @@ -135,15 +136,17 @@ cw4 = { version = "=1.1.0" } cw-controllers = { version = "=1.1.0" } dotenvy = "0.15.6" generic-array = "0.14.7" -k256 = "0.13" getrandom = "0.2.10" +k256 = "0.13" lazy_static = "1.4.0" log = "0.4" once_cell = "1.7.2" rand = "0.8.5" +reqwest = "0.11.18" serde = "1.0.152" serde_json = "1.0.91" tap = "1.0.1" +tendermint-rpc = "=0.32" # same version as used by cosmrs thiserror = "1.0.38" tokio = "1.24.1" url = "2.2" diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs index 76337b84ac..ef370c54b8 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -94,6 +94,7 @@ impl From for OverrideConfig { use_anonymous_replies: init_config.use_reply_surbs, fastmode: init_config.fastmode, no_cover: init_config.no_cover, + geo_routing: None, medium_toggle: false, nyxd_urls: init_config.nyxd_urls, enabled_credentials_mode: init_config.enabled_credentials_mode, diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 5ef887a3a2..393429246d 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -16,7 +16,8 @@ use nym_client_core::client::base_client::storage::gateway_details::{ OnDiskGatewayDetails, PersistedGatewayDetails, }; use nym_client_core::client::key_manager::persistence::OnDiskKeys; -use nym_client_core::config::GatewayEndpointConfig; +use nym_client_core::client::topology_control::geo_aware_provider::CountryGroup; +use nym_client_core::config::{GatewayEndpointConfig, TopologyStructure}; use nym_client_core::error::ClientCoreError; use nym_config::OptionalSet; use nym_sphinx::params::{PacketSize, PacketType}; @@ -75,6 +76,7 @@ pub(crate) struct OverrideConfig { use_anonymous_replies: Option, fastmode: bool, no_cover: bool, + geo_routing: Option, medium_toggle: bool, nyxd_urls: Option>, enabled_credentials_mode: Option, @@ -99,6 +101,13 @@ pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config { let secondary_packet_size = args.medium_toggle.then_some(PacketSize::ExtendedPacket16); let no_per_hop_delays = args.medium_toggle; + let topology_structure = if args.medium_toggle || args.geo_routing.is_some() { + // TODO: rethink the default group. I just picked one for now. + TopologyStructure::GeoAware(args.geo_routing.unwrap_or(CountryGroup::Europe)) + } else { + TopologyStructure::default() + }; + let packet_type = if args.outfox { PacketType::Outfox } else { @@ -122,6 +131,10 @@ pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config { // NOTE: see comment above about the order of the other disble cover traffic config .with_base(BaseClientConfig::with_disabled_cover_traffic, args.no_cover) .with_base(BaseClientConfig::with_packet_type, packet_type) + .with_base( + BaseClientConfig::with_topology_structure, + topology_structure, + ) .with_optional(Config::with_anonymous_replies, args.use_anonymous_replies) .with_optional(Config::with_port, args.port) .with_optional_base_custom_env( diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index 5d54038c06..8dd289e2bf 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -11,6 +11,7 @@ use clap::Args; use log::*; use nym_bin_common::version_checker::is_minor_version_compatible; use nym_client_core::client::base_client::storage::OnDiskPersistent; +use nym_client_core::client::topology_control::geo_aware_provider::CountryGroup; use nym_crypto::asymmetric::identity; use nym_socks5_client_core::NymClient; use nym_sphinx::addressing::clients::Recipient; @@ -60,6 +61,10 @@ pub(crate) struct Run { #[clap(long, hide = true)] no_cover: bool, + /// Set geo-aware mixnode selection when sending mixnet traffic, for experiments only. + #[clap(long, hide = true, value_parser = validate_country_group)] + geo_routing: Option, + /// Enable medium mixnet traffic, for experiments only. /// This includes things like disabling cover traffic, no per hop delays, etc. #[clap(long, hide = true)] @@ -82,6 +87,7 @@ impl From for OverrideConfig { use_anonymous_replies: run_config.use_anonymous_replies, fastmode: run_config.fastmode, no_cover: run_config.no_cover, + geo_routing: run_config.geo_routing, medium_toggle: run_config.medium_toggle, nyxd_urls: run_config.nyxd_urls, enabled_credentials_mode: run_config.enabled_credentials_mode, @@ -90,6 +96,13 @@ impl From for OverrideConfig { } } +fn validate_country_group(s: &str) -> Result { + match s.parse() { + Ok(cg) => Ok(cg), + Err(_) => Err(format!("failed to parse country group: {}", s)), + } +} + // this only checks compatibility between config the binary. It does not take into consideration // network version. It might do so in the future. fn version_check(cfg: &Config) -> bool { diff --git a/clients/webassembly/Cargo.lock b/clients/webassembly/Cargo.lock index 793f321a97..0ea047444e 100644 --- a/clients/webassembly/Cargo.lock +++ b/clients/webassembly/Cargo.lock @@ -2475,6 +2475,7 @@ dependencies = [ "nym-config", "nym-credential-storage", "nym-crypto", + "nym-explorer-api-requests", "nym-gateway-client", "nym-gateway-requests", "nym-network-defaults", @@ -2485,6 +2486,7 @@ dependencies = [ "nym-topology", "nym-validator-client", "rand 0.7.3", + "reqwest", "serde", "serde_json", "sha2 0.10.6", @@ -2689,6 +2691,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-explorer-api-requests" +version = "0.1.0" +dependencies = [ + "nym-contracts-common", + "nym-mixnet-contract-common", + "nym-validator-client", + "schemars", + "serde", +] + [[package]] name = "nym-gateway-client" version = "0.1.0" diff --git a/clients/webassembly/src/client/config.rs b/clients/webassembly/src/client/config.rs index 07d9ab5f5f..f63d1456ac 100644 --- a/clients/webassembly/src/client/config.rs +++ b/clients/webassembly/src/client/config.rs @@ -246,6 +246,7 @@ impl From for ConfigTopology { topology.topology_resolution_timeout_ms, ), disable_refreshing: topology.disable_refreshing, + topology_structure: Default::default(), } } } diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index ab1c9b8c04..0811b08add 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -10,27 +10,29 @@ rust-version = "1.66" [dependencies] async-trait = { workspace = true } base64 = "0.21.2" -dirs = "4.0" dashmap = "5.4.0" +dirs = "4.0" futures = "0.3" humantime-serde = "1.0" log = { workspace = true } rand = { version = "0.7.3", features = ["wasm-bindgen"] } +reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha2 = "0.10.6" tap = "1.0.1" thiserror = "1.0.34" -url = { version ="2.2", features = ["serde"] } -tungstenite = { version = "0.13.0", default-features = false } -tokio = { version = "1.24.1", features = ["macros"]} time = "0.3.17" +tokio = { version = "1.24.1", features = ["macros"]} +tungstenite = { version = "0.13.0", default-features = false } +url = { version ="2.2", features = ["serde"] } zeroize = { workspace = true } # internal nym-bandwidth-controller = { path = "../bandwidth-controller" } nym-config = { path = "../config" } nym-crypto = { path = "../crypto" } +nym-explorer-api-requests = { path = "../../explorer-api/explorer-api-requests" } nym-gateway-client = { path = "../client-libs/gateway-client" } #gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm", "coconut"] } nym-gateway-requests = { path = "../../gateway/gateway-requests" } diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index aecb7eb5c1..a934793953 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use super::received_buffer::ReceivedBufferMessage; +use super::topology_control::geo_aware_provider::GeoAwareTopologyProvider; use crate::client::base_client::storage::MixnetClientStorage; use crate::client::cover_traffic_stream::LoopCoverTrafficStream; use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}; @@ -339,14 +340,20 @@ where fn setup_topology_provider( custom_provider: Option>, + provider_from_config: config::TopologyStructure, nym_api_urls: Vec, ) -> Box { // if no custom provider was ... provided ..., create one using nym-api - custom_provider.unwrap_or_else(|| { - Box::new(NymApiTopologyProvider::new( + custom_provider.unwrap_or_else(|| match provider_from_config { + config::TopologyStructure::NymApi => Box::new(NymApiTopologyProvider::new( nym_api_urls, env!("CARGO_PKG_VERSION").to_string(), - )) + )), + config::TopologyStructure::GeoAware(group) => Box::new(GeoAwareTopologyProvider::new( + nym_api_urls, + env!("CARGO_PKG_VERSION").to_string(), + group, + )), }) } @@ -521,8 +528,10 @@ where let topology_provider = Self::setup_topology_provider( self.custom_topology_provider.take(), + self.config.debug.topology.topology_structure, self.config.get_nym_api_endpoints(), ); + Self::start_topology_refresher( topology_provider, self.config.debug.topology, diff --git a/common/client-core/src/client/topology_control/geo_aware_provider.rs b/common/client-core/src/client/topology_control/geo_aware_provider.rs new file mode 100644 index 0000000000..268f6612e0 --- /dev/null +++ b/common/client-core/src/client/topology_control/geo_aware_provider.rs @@ -0,0 +1,319 @@ +use std::{collections::HashMap, fmt}; + +use log::{debug, error, info}; +use nym_explorer_api_requests::PrettyDetailedMixNodeBond; +use nym_topology::{ + nym_topology_from_detailed, + provider_trait::{async_trait, TopologyProvider}, + NymTopology, +}; +use nym_validator_client::client::MixId; +use rand::{prelude::SliceRandom, thread_rng}; +use serde::{Deserialize, Serialize}; +use url::Url; + +const MIN_NODES_PER_LAYER: usize = 1; +const EXPLORER_API_MIXNODES_URL: &str = "https://explorer.nymtech.net/api/v1/mix-nodes"; + +// TODO: create a explorer-api-client +async fn fetch_mixnodes_from_explorer_api() -> Option> { + reqwest::get(EXPLORER_API_MIXNODES_URL) + .await + .ok()? + .json::>() + .await + .ok() +} + +#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)] +pub enum CountryGroup { + Europe, + NorthAmerica, + SouthAmerica, + Oceania, + Asia, + Africa, + Unknown, +} + +impl CountryGroup { + // We map contry codes into group, which initially are continent codes to a first approximation, + // but we do it manually to reserve the right to tweak this distribution for our purposes. + fn new(country_code: &str) -> Self { + let country_code = country_code.to_uppercase(); + use CountryGroup::*; + match country_code.as_ref() { + // Europe + "AT" => Europe, + "BG" => Europe, + "CH" => Europe, + "CY" => Europe, + "CZ" => Europe, + "DE" => Europe, + "DK" => Europe, + "ES" => Europe, + "FI" => Europe, + "FR" => Europe, + "GB" => Europe, + "GR" => Europe, + "IE" => Europe, + "IT" => Europe, + "LT" => Europe, + "LU" => Europe, + "LV" => Europe, + "MD" => Europe, + "MT" => Europe, + "NL" => Europe, + "NO" => Europe, + "PL" => Europe, + "RO" => Europe, + "SE" => Europe, + "SK" => Europe, + "TR" => Europe, + "UA" => Europe, + + // North America + "CA" => NorthAmerica, + "MX" => NorthAmerica, + "US" => NorthAmerica, + + // South America + "AR" => SouthAmerica, + "BR" => SouthAmerica, + "CL" => SouthAmerica, + "CO" => SouthAmerica, + "CR" => SouthAmerica, + "GT" => SouthAmerica, + + // Oceania + "AU" => Oceania, + + // Asia + "AM" => Asia, + "BH" => Asia, + "CN" => Asia, + "GE" => Asia, + "HK" => Asia, + "ID" => Asia, + "IL" => Asia, + "IN" => Asia, + "JP" => Asia, + "KH" => Asia, + "KR" => Asia, + "KZ" => Asia, + "MY" => Asia, + "RU" => Asia, + "SG" => Asia, + "TH" => Asia, + "VN" => Asia, + + // Africa + "SC" => Africa, + "UG" => Africa, + "ZA" => Africa, + + // And group level codes work too + "EU" => Europe, + "NA" => NorthAmerica, + "SA" => SouthAmerica, + "OC" => Oceania, + "AS" => Asia, + "AF" => Africa, + + // And some aliases + "EUROPE" => Europe, + "NORTHAMERICA" => NorthAmerica, + "SOUTHAMERICA" => SouthAmerica, + "OCEANIA" => Oceania, + "ASIA" => Asia, + "AFRICA" => Africa, + + _ => { + info!("Unknown country code: {}", country_code); + Unknown + } + } + } +} + +impl fmt::Display for CountryGroup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use CountryGroup::*; + match self { + Europe => write!(f, "EU"), + NorthAmerica => write!(f, "NA"), + SouthAmerica => write!(f, "SA"), + Oceania => write!(f, "OC"), + Asia => write!(f, "AS"), + Africa => write!(f, "AF"), + Unknown => write!(f, "Unknown"), + } + } +} + +impl std::str::FromStr for CountryGroup { + type Err = (); + + fn from_str(s: &str) -> Result { + let group = CountryGroup::new(s); + if group == CountryGroup::Unknown { + Err(()) + } else { + Ok(group) + } + } +} + +impl CountryGroup { + #[allow(unused)] + fn known(self) -> Option { + use CountryGroup::*; + match self { + Europe | NorthAmerica | SouthAmerica | Oceania | Asia | Africa => Some(self), + Unknown => None, + } + } +} + +fn group_mixnodes_by_country_code( + mixnodes: Vec, +) -> HashMap> { + mixnodes + .into_iter() + .fold(HashMap::>::new(), |mut acc, m| { + if let Some(ref location) = m.location { + let country_code = location.two_letter_iso_country_code.clone(); + let group_code = CountryGroup::new(country_code.as_str()); + let mixnodes = acc.entry(group_code).or_insert_with(Vec::new); + mixnodes.push(m.mix_id); + } + acc + }) +} + +fn log_mixnode_distribution(mixnodes: &HashMap>) { + let mixnode_distribution = mixnodes + .iter() + .map(|(k, v)| format!("{}: {}", k, v.len())) + .collect::>() + .join(", "); + debug!("Mixnode distribution - {}", mixnode_distribution); +} + +fn check_layer_integrity(topology: NymTopology) -> Result<(), ()> { + let mixes = topology.mixes(); + if mixes.keys().len() < 3 { + error!("Layer is missing in topology!"); + return Err(()); + } + for (layer, mixnodes) in mixes { + debug!("Layer {:?} has {} mixnodes", layer, mixnodes.len()); + if mixnodes.len() < MIN_NODES_PER_LAYER { + error!( + "There are only {} mixnodes in layer {:?}", + mixnodes.len(), + layer + ); + return Err(()); + } + } + Ok(()) +} + +pub struct GeoAwareTopologyProvider { + validator_client: nym_validator_client::client::NymApiClient, + filter_on: CountryGroup, + client_version: String, +} + +impl GeoAwareTopologyProvider { + pub fn new( + mut nym_api_urls: Vec, + client_version: String, + filter_on: CountryGroup, + ) -> GeoAwareTopologyProvider { + log::info!( + "Creating geo-aware topology provider with filter on {:?}", + filter_on + ); + nym_api_urls.shuffle(&mut thread_rng()); + + GeoAwareTopologyProvider { + validator_client: nym_validator_client::client::NymApiClient::new( + nym_api_urls[0].clone(), + ), + filter_on, + client_version, + } + } + + async fn get_topology(&self) -> Option { + let mixnodes = match self.validator_client.get_cached_active_mixnodes().await { + Err(err) => { + error!("failed to get network mixnodes - {err}"); + return None; + } + Ok(mixes) => mixes, + }; + + let gateways = match self.validator_client.get_cached_gateways().await { + Err(err) => { + error!("failed to get network gateways - {err}"); + return None; + } + Ok(gateways) => gateways, + }; + + // Also fetch mixnodes cached by explorer-api, with the purpose of getting their + // geolocation. + debug!("Fetching mixnodes from explorer-api..."); + let Some(mixnodes_from_explorer_api) = fetch_mixnodes_from_explorer_api().await else { + error!("failed to get mixnodes from explorer-api"); + return None; + }; + + // Partition mixnodes_from_explorer_api according to the value of + // two_letter_iso_country_code. + // NOTE: we construct the full distribution here, but only use the one we're interested in. + // The reason we this instead of a straight filter is that this opens up the possibility to + // complement a small grouping with mixnodes from adjecent countries. + let mixnode_distribution = group_mixnodes_by_country_code(mixnodes_from_explorer_api); + log_mixnode_distribution(&mixnode_distribution); + + let Some(filtered_mixnode_ids) = mixnode_distribution.get(&self.filter_on) else { + error!("no mixnodes found for: {}", self.filter_on); + return None; + }; + + let mixnodes = mixnodes + .into_iter() + .filter(|m| filtered_mixnode_ids.contains(&m.mix_id())) + .collect::>(); + + let topology = nym_topology_from_detailed(mixnodes, gateways) + .filter_system_version(&self.client_version); + + // TODO: return real error type + check_layer_integrity(topology.clone()).ok()?; + + Some(topology) + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[async_trait] +impl TopologyProvider for GeoAwareTopologyProvider { + // this will be manually refreshed on a timer specified inside mixnet client config + async fn get_new_topology(&mut self) -> Option { + self.get_topology().await + } +} + +#[cfg(target_arch = "wasm32")] +#[async_trait(?Send)] +impl TopologyProvider for GeoAwareTopologyProvider { + // this will be manually refreshed on a timer specified inside mixnet client config + async fn get_new_topology(&mut self) -> Option { + self.get_topology().await + } +} diff --git a/common/client-core/src/client/topology_control/mod.rs b/common/client-core/src/client/topology_control/mod.rs index 0fef523b1e..ed426dde50 100644 --- a/common/client-core/src/client/topology_control/mod.rs +++ b/common/client-core/src/client/topology_control/mod.rs @@ -10,6 +10,7 @@ use nym_topology::NymTopologyError; use std::time::Duration; mod accessor; +pub mod geo_aware_provider; pub(crate) mod nym_api_provider; // TODO: move it to config later diff --git a/common/client-core/src/config/mod.rs b/common/client-core/src/config/mod.rs index e8f08adfaa..465c227e19 100644 --- a/common/client-core/src/config/mod.rs +++ b/common/client-core/src/config/mod.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use std::time::Duration; use url::Url; -use crate::error::ClientCoreError; +use crate::{client::topology_control::geo_aware_provider::CountryGroup, error::ClientCoreError}; #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; @@ -158,6 +158,15 @@ impl Config { self } + pub fn with_topology_structure(mut self, topology_structure: TopologyStructure) -> Self { + self.set_topology_structure(topology_structure); + self + } + + pub fn set_topology_structure(&mut self, topology_structure: TopologyStructure) { + self.debug.topology.topology_structure = topology_structure; + } + pub fn with_no_per_hop_delays(mut self, no_per_hop_delays: bool) -> Self { if no_per_hop_delays { self.set_no_per_hop_delays() @@ -466,6 +475,16 @@ pub struct Topology { /// the first valid instance. /// Supersedes `topology_refresh_rate_ms`. pub disable_refreshing: bool, + + /// Specifies the mixnode topology to be used for sending packets. + pub topology_structure: TopologyStructure, +} + +#[derive(Default, Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum TopologyStructure { + #[default] + NymApi, + GeoAware(CountryGroup), } impl Default for Topology { @@ -474,6 +493,7 @@ impl Default for Topology { topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE, topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT, disable_refreshing: false, + topology_structure: TopologyStructure::default(), } } } diff --git a/common/client-core/src/config/old_config_v1_1_20_2.rs b/common/client-core/src/config/old_config_v1_1_20_2.rs index ed0cd0356a..a097b55f2a 100644 --- a/common/client-core/src/config/old_config_v1_1_20_2.rs +++ b/common/client-core/src/config/old_config_v1_1_20_2.rs @@ -267,6 +267,7 @@ impl From for Topology { topology_refresh_rate: value.topology_refresh_rate, topology_resolution_timeout: value.topology_resolution_timeout, disable_refreshing: value.disable_refreshing, + topology_structure: Default::default(), } } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 248bc7e143..676098fce6 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -69,6 +69,10 @@ impl MixNodeDetails { self.bond_information.mix_id } + pub fn layer(&self) -> Layer { + self.bond_information.layer + } + pub fn is_unbonding(&self) -> bool { self.bond_information.is_unbonding } diff --git a/common/socks5-client-core/Cargo.toml b/common/socks5-client-core/Cargo.toml index 9ca792f8b1..2e1aa52b32 100644 --- a/common/socks5-client-core/Cargo.toml +++ b/common/socks5-client-core/Cargo.toml @@ -7,22 +7,27 @@ edition = "2021" [dependencies] dirs = "4.0" +futures = "0.3" log = { workspace = true } pin-project = "1.0" rand = { version = "0.7.3", features = ["wasm-bindgen"] } +reqwest = "0.11.4" +schemars = { version = "0.8", features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } # for config serialization/deserialization -thiserror = "1.0.34" tap = "1.0.1" +thiserror = "1.0.34" tokio = { version = "1.24.1", features = ["rt-multi-thread", "net", "signal"] } -futures = "0.3" +url = "2.2" -nym-client-core = { path = "../client-core", features = ["fs-surb-storage"] } nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } +nym-client-core = { path = "../client-core", features = ["fs-surb-storage"] } nym-config = { path = "../config" } +nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common" } nym-credential-storage = { path = "../credential-storage" } +nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } nym-network-defaults = { path = "../network-defaults" } -nym-socks5-proxy-helpers = { path = "../socks5/proxy-helpers" } nym-service-providers-common = { path = "../../service-providers/common" } +nym-socks5-proxy-helpers = { path = "../socks5/proxy-helpers" } nym-socks5-requests = { path = "../socks5/requests" } nym-sphinx = { path = "../nymsphinx" } nym-task = { path = "../task" } diff --git a/common/socks5-client-core/src/lib.rs b/common/socks5-client-core/src/lib.rs index 2070eeaf71..74d27b69ed 100644 --- a/common/socks5-client-core/src/lib.rs +++ b/common/socks5-client-core/src/lib.rs @@ -24,6 +24,7 @@ use nym_credential_storage::storage::Storage as CredentialStorage; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::params::PacketType; use nym_task::{TaskClient, TaskManager}; + use std::error::Error; pub mod config; diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index ab0a56612b..d6941a19e1 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -8,13 +8,18 @@ edition = "2021" [dependencies] chrono = { version = "0.4.19", features = ["serde"] } clap = { version = "4.0", features = ["cargo", "derive"] } +dotenvy = "0.15.6" humantime-serde = "1.0" isocountry = "0.3.2" itertools = "0.10.3" lazy_static = "1.4.0" log = { workspace = true } +maxminddb = "0.23.0" okapi = { version = "0.7.0-rc.1", features = ["impl_json_schema"] } pretty_env_logger = "0.4.0" +rand = "0.8.5" +rand_pcg = "0.3.1" +rand_seeder = "0.2.3" reqwest = "0.11.4" rocket = { version = "0.5.0-rc.2", features = ["json"] } rocket_cors = { git="https://github.com/lawliet89/rocket_cors", rev="dfd3662c49e2f6fc37df35091cb94d82f7fb5915" } @@ -24,15 +29,11 @@ serde = "1.0.126" serde_json = "1.0.66" thiserror = "1.0.29" tokio = {version = "1.21.2", features = ["full"] } -maxminddb = "0.23.0" -dotenvy = "0.15.6" -rand = "0.8.5" -rand_seeder = "0.2.3" -rand_pcg = "0.3.1" -nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } -nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } -nym-network-defaults = { path = "../common/network-defaults" } nym-bin-common = { path = "../common/bin-common"} +nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } +nym-explorer-api-requests = { path = "explorer-api-requests" } +nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } +nym-network-defaults = { path = "../common/network-defaults" } nym-task = { path = "../common/task" } nym-validator-client = { path = "../common/client-libs/validator-client", features=["http-client"] } diff --git a/explorer-api/explorer-api-requests/Cargo.toml b/explorer-api/explorer-api-requests/Cargo.toml new file mode 100644 index 0000000000..93c8faaada --- /dev/null +++ b/explorer-api/explorer-api-requests/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "nym-explorer-api-requests" +version = "0.1.0" +edition = "2021" + +[dependencies] +nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } +nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } +nym-validator-client = { path = "../../common/client-libs/validator-client" } +schemars = { version = "0.8", features = ["preserve_order"] } +serde = { version = "1.0", features = ["derive"] } +ts-rs = { version = "6.1.2", optional = true } diff --git a/explorer-api/explorer-api-requests/src/lib.rs b/explorer-api/explorer-api-requests/src/lib.rs new file mode 100644 index 0000000000..bfcafa2860 --- /dev/null +++ b/explorer-api/explorer-api-requests/src/lib.rs @@ -0,0 +1,44 @@ +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::{Addr, Coin, Layer, MixId, MixNode}; +use nym_validator_client::models::NodePerformance; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum MixnodeStatus { + Active, // in both the active set and the rewarded set + Standby, // only in the rewarded set + Inactive, // in neither the rewarded set nor the active set +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct PrettyDetailedMixNodeBond { + pub mix_id: MixId, + pub location: Option, + pub status: MixnodeStatus, + pub pledge_amount: Coin, + pub total_delegation: Coin, + pub owner: Addr, + pub layer: Layer, + pub mix_node: MixNode, + pub stake_saturation: f32, + pub uncapped_saturation: f32, + pub avg_uptime: u8, + pub node_performance: NodePerformance, + pub estimated_operator_apy: f64, + pub estimated_delegators_apy: f64, + pub operating_cost: Coin, + pub profit_margin_percent: Percent, + pub family_id: Option, + pub blacklisted: bool, +} + +#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize)] +pub struct Location { + pub two_letter_iso_country_code: String, + pub three_letter_iso_country_code: String, + pub country_name: String, + pub latitude: Option, + pub longitude: Option, +} diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index 8158c6e65e..a80ebc28e4 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -1,9 +1,9 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mix_nodes::location::Location; use crate::state::ExplorerApiStateContext; use log::{info, warn}; +use nym_explorer_api_requests::Location; use nym_task::TaskClient; pub(crate) struct GeoLocateTask { @@ -64,7 +64,7 @@ impl GeoLocateTask { ) { Ok(opt) => match opt { Some(location) => { - let location = Location::new(location); + let location: Location = location.into(); trace!( "{} mix nodes already located. Ip {} is located in {:#?}", diff --git a/explorer-api/src/geo_ip/location.rs b/explorer-api/src/geo_ip/location.rs index adbdb6cef3..fdb8584938 100644 --- a/explorer-api/src/geo_ip/location.rs +++ b/explorer-api/src/geo_ip/location.rs @@ -42,6 +42,18 @@ pub(crate) struct Location { pub(crate) longitude: Option, } +impl From for nym_explorer_api_requests::Location { + fn from(location: Location) -> Self { + nym_explorer_api_requests::Location { + country_name: location.name, + two_letter_iso_country_code: location.iso_alpha2, + three_letter_iso_country_code: location.iso_alpha3, + latitude: location.latitude, + longitude: location.longitude, + } + } +} + impl GeoIp { pub fn new() -> Self { let db_path = std::env::var("GEOIP_DB_PATH").unwrap_or_else(|e| { diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index 137c6250ed..e00ed8ff82 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -6,9 +6,10 @@ use crate::mix_node::delegations::{ }; use crate::mix_node::econ_stats::retrieve_mixnode_econ_stats; use crate::mix_node::models::{ - EconomicDynamicsStats, NodeDescription, NodeStats, PrettyDetailedMixNodeBond, SummedDelegations, + EconomicDynamicsStats, NodeDescription, NodeStats, SummedDelegations, }; use crate::state::ExplorerApiStateContext; +use nym_explorer_api_requests::PrettyDetailedMixNodeBond; use nym_mixnet_contract_common::{Delegation, MixId}; use reqwest::Error as ReqwestError; use rocket::response::status::NotFound; diff --git a/explorer-api/src/mix_node/mod.rs b/explorer-api/src/mix_node/mod.rs index 88c3b1cd71..dac365c71d 100644 --- a/explorer-api/src/mix_node/mod.rs +++ b/explorer-api/src/mix_node/mod.rs @@ -1,4 +1,4 @@ pub(crate) mod delegations; pub(crate) mod econ_stats; pub(crate) mod http; -pub(crate) mod models; +pub mod models; diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 94ae6558d0..4adfa6dd86 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -2,49 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use crate::cache::Cache; -use crate::mix_nodes::location::Location; -use nym_contracts_common::Percent; use nym_mixnet_contract_common::Delegation; -use nym_mixnet_contract_common::{Addr, Coin, Layer, MixId, MixNode}; -use nym_validator_client::models::{NodePerformance, SelectionChance}; +use nym_mixnet_contract_common::{Addr, Coin, MixId}; +use nym_validator_client::models::SelectionChance; use serde::Deserialize; use serde::Serialize; use std::sync::Arc; use std::time::SystemTime; use tokio::sync::RwLock; -#[derive(Clone, Debug, Serialize, JsonSchema, PartialEq)] -#[serde(rename_all = "snake_case")] -pub(crate) enum MixnodeStatus { - Active, // in both the active set and the rewarded set - Standby, // only in the rewarded set - Inactive, // in neither the rewarded set nor the active set -} - -#[derive(Clone, Debug, Serialize, JsonSchema)] -pub(crate) struct PrettyDetailedMixNodeBond { - // I leave this to @MS to refactor this type as a lot of things here are redundant thanks to - // the existence of `MixNodeDetails` - pub mix_id: MixId, - pub location: Option, - pub status: MixnodeStatus, - pub pledge_amount: Coin, - pub total_delegation: Coin, - pub owner: Addr, - pub layer: Layer, - pub mix_node: MixNode, - pub stake_saturation: f32, - pub uncapped_saturation: f32, - pub avg_uptime: u8, - pub node_performance: NodePerformance, - pub estimated_operator_apy: f64, - pub estimated_delegators_apy: f64, - pub operating_cost: Coin, - pub profit_margin_percent: Percent, - pub family_id: Option, - pub blacklisted: bool, -} - #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct SummedDelegations { pub owner: Addr, diff --git a/explorer-api/src/mix_nodes/http.rs b/explorer-api/src/mix_nodes/http.rs index ef5570e455..a36843b66f 100644 --- a/explorer-api/src/mix_nodes/http.rs +++ b/explorer-api/src/mix_nodes/http.rs @@ -1,6 +1,6 @@ -use crate::mix_node::models::{MixnodeStatus, PrettyDetailedMixNodeBond}; use crate::mix_nodes::models::{MixNodeActiveSetSummary, MixNodeSummary}; use crate::state::ExplorerApiStateContext; +use nym_explorer_api_requests::{MixnodeStatus, PrettyDetailedMixNodeBond}; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; diff --git a/explorer-api/src/mix_nodes/location.rs b/explorer-api/src/mix_nodes/location.rs index b876cdc792..4908564697 100644 --- a/explorer-api/src/mix_nodes/location.rs +++ b/explorer-api/src/mix_nodes/location.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::geo_ip::location; +use nym_explorer_api_requests::Location; use nym_mixnet_contract_common::MixId; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -31,27 +31,6 @@ pub(crate) struct LocationCacheItem { pub(crate) valid_until: SystemTime, } -#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize)] -pub(crate) struct Location { - pub(crate) two_letter_iso_country_code: String, - pub(crate) three_letter_iso_country_code: String, - pub(crate) country_name: String, - pub(crate) latitude: Option, - pub(crate) longitude: Option, -} - -impl Location { - pub(crate) fn new(location: location::Location) -> Self { - Location { - country_name: location.name, - two_letter_iso_country_code: location.iso_alpha2, - three_letter_iso_country_code: location.iso_alpha3, - latitude: location.latitude, - longitude: location.longitude, - } - } -} - impl LocationCacheItem { pub(crate) fn new_from_location(location: Option) -> Self { LocationCacheItem { diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index bfead42c8c..b107bb2168 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -5,6 +5,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{Duration, SystemTime}; +use nym_explorer_api_requests::{Location, MixnodeStatus, PrettyDetailedMixNodeBond}; use nym_mixnet_contract_common::rewarding::helpers::truncate_reward; use nym_mixnet_contract_common::MixId; use serde::Serialize; @@ -14,8 +15,7 @@ use crate::helpers::best_effort_small_dec_to_f64; use nym_validator_client::models::MixNodeBondAnnotated; use super::utils::family_numerical_id; -use crate::mix_node::models::{MixnodeStatus, PrettyDetailedMixNodeBond}; -use crate::mix_nodes::location::{Location, LocationCache, LocationCacheItem}; +use crate::mix_nodes::location::{LocationCache, LocationCacheItem}; use crate::mix_nodes::CACHE_ENTRY_TTL; #[derive(Clone, Debug, Serialize, JsonSchema)] diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 5897f51672..c522d297d1 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -3559,6 +3559,7 @@ dependencies = [ "nym-config", "nym-credential-storage", "nym-crypto", + "nym-explorer-api-requests", "nym-gateway-client", "nym-gateway-requests", "nym-network-defaults", @@ -3569,6 +3570,7 @@ dependencies = [ "nym-topology", "nym-validator-client", "rand 0.7.3", + "reqwest", "serde", "serde_json", "sha2 0.10.6", @@ -3780,6 +3782,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-explorer-api-requests" +version = "0.1.0" +dependencies = [ + "nym-contracts-common", + "nym-mixnet-contract-common", + "nym-validator-client", + "schemars", + "serde", +] + [[package]] name = "nym-gateway-client" version = "0.1.0" @@ -3979,7 +3992,9 @@ dependencies = [ "nym-bandwidth-controller", "nym-client-core", "nym-config", + "nym-contracts-common", "nym-credential-storage", + "nym-mixnet-contract-common", "nym-network-defaults", "nym-service-providers-common", "nym-socks5-proxy-helpers", @@ -3989,10 +4004,13 @@ dependencies = [ "nym-validator-client", "pin-project", "rand 0.7.3", + "reqwest", + "schemars", "serde", "tap", "thiserror", "tokio", + "url", ] [[package]] @@ -5121,9 +5139,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "reqwest" -version = "0.11.15" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ba30cc2c0cd02af1222ed216ba659cdb2f879dfe3181852fe7c50b1d0005949" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ "base64 0.21.2", "bytes", diff --git a/nym-connect/desktop/src-tauri/src/tasks.rs b/nym-connect/desktop/src-tauri/src/tasks.rs index 8c69b1be7e..06b0809c2f 100644 --- a/nym-connect/desktop/src-tauri/src/tasks.rs +++ b/nym-connect/desktop/src-tauri/src/tasks.rs @@ -1,17 +1,23 @@ use futures::{channel::mpsc, StreamExt}; -use nym_client_core::client::base_client::storage::gateway_details::GatewayDetailsStore; -use nym_client_core::client::base_client::storage::{MixnetClientStorage, OnDiskPersistent}; -use nym_client_core::{config::GatewayEndpointConfig, error::ClientCoreStatusMessage}; -use nym_socks5_client_core::NymClient as Socks5NymClient; -use nym_socks5_client_core::Socks5ControlMessageSender; +use nym_client_core::{ + client::{ + base_client::storage::{ + gateway_details::GatewayDetailsStore, MixnetClientStorage, OnDiskPersistent, + }, + topology_control::geo_aware_provider::CountryGroup, + }, + config::{GatewayEndpointConfig, TopologyStructure}, + error::ClientCoreStatusMessage, +}; +use nym_socks5_client_core::{NymClient as Socks5NymClient, Socks5ControlMessageSender}; use nym_sphinx::params::PacketSize; use nym_task::manager::TaskStatus; use std::sync::Arc; use tap::TapFallible; use tokio::sync::RwLock; -use crate::config::{Config, PrivacyLevel}; use crate::{ + config::{Config, PrivacyLevel}, error::Result, events::{self, emit_event, emit_status_event}, models::{ConnectionStatusKind, ConnectivityTestResult}, @@ -46,6 +52,14 @@ fn override_config_from_env(config: &mut Config, privacy_level: &PrivacyLevel) { log::warn!("Disabling per-hop delay"); config.core.base.set_no_per_hop_delays(); + + // TODO: selectable in the UI + let default_country_group = CountryGroup::Europe; + log::warn!("Using geo-aware mixnode selection: {default_country_group}"); + config + .core + .base + .set_topology_structure(TopologyStructure::GeoAware(default_country_group)); } } diff --git a/service-providers/network-requester/src/allowed_hosts/hosts.rs b/service-providers/network-requester/src/allowed_hosts/hosts.rs index 5c48369d0d..11aa3dd276 100644 --- a/service-providers/network-requester/src/allowed_hosts/hosts.rs +++ b/service-providers/network-requester/src/allowed_hosts/hosts.rs @@ -136,7 +136,6 @@ impl HostsStore { }) .map(Host::from) .collect(); - dbg!(&hosts); Ok(hosts) } }