Add geo-aware mixnet topology provider (#3713)

* WIP: initial work

* wupwup

* WIP: experiments

* Move topology provider and requests to own crate

* Make sure we use the new crate everywhere

* Sort Cargo.toml

* Extract out some functions in geo_aware_provider

* rustfmt

* Add CountryGroup type

* Assign unknown as well

* wipwip

* Add command line flag to socks5-client

* Use geo-aware mixnode selection in nym-connect when in medium mode

* rustfmt

* clippy

* Fix nym-connect build

* wasm fix

* Spelling
This commit is contained in:
Jon Häggblad
2023-07-28 14:48:33 +02:00
committed by benedettadavico
parent a86c1a6a60
commit e00910bcb8
30 changed files with 568 additions and 96 deletions
Generated
+20
View File
@@ -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]]
+4 -1
View File
@@ -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"
+1
View File
@@ -94,6 +94,7 @@ impl From<Init> 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,
+14 -1
View File
@@ -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<bool>,
fastmode: bool,
no_cover: bool,
geo_routing: Option<CountryGroup>,
medium_toggle: bool,
nyxd_urls: Option<Vec<url::Url>>,
enabled_credentials_mode: Option<bool>,
@@ -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(
+13
View File
@@ -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<CountryGroup>,
/// 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<Run> 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<Run> for OverrideConfig {
}
}
fn validate_country_group(s: &str) -> Result<CountryGroup, String> {
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 {
+13
View File
@@ -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"
+1
View File
@@ -246,6 +246,7 @@ impl From<TopologyWasm> for ConfigTopology {
topology.topology_resolution_timeout_ms,
),
disable_refreshing: topology.disable_refreshing,
topology_structure: Default::default(),
}
}
}
+6 -4
View File
@@ -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" }
@@ -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<Box<dyn TopologyProvider + Send + Sync>>,
provider_from_config: config::TopologyStructure,
nym_api_urls: Vec<Url>,
) -> Box<dyn TopologyProvider + Send + Sync> {
// 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,
@@ -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<Vec<PrettyDetailedMixNodeBond>> {
reqwest::get(EXPLORER_API_MIXNODES_URL)
.await
.ok()?
.json::<Vec<PrettyDetailedMixNodeBond>>()
.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<Self, Self::Err> {
let group = CountryGroup::new(s);
if group == CountryGroup::Unknown {
Err(())
} else {
Ok(group)
}
}
}
impl CountryGroup {
#[allow(unused)]
fn known(self) -> Option<CountryGroup> {
use CountryGroup::*;
match self {
Europe | NorthAmerica | SouthAmerica | Oceania | Asia | Africa => Some(self),
Unknown => None,
}
}
}
fn group_mixnodes_by_country_code(
mixnodes: Vec<PrettyDetailedMixNodeBond>,
) -> HashMap<CountryGroup, Vec<MixId>> {
mixnodes
.into_iter()
.fold(HashMap::<CountryGroup, Vec<MixId>>::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<CountryGroup, Vec<MixId>>) {
let mixnode_distribution = mixnodes
.iter()
.map(|(k, v)| format!("{}: {}", k, v.len()))
.collect::<Vec<_>>()
.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<Url>,
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<NymTopology> {
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::<Vec<_>>();
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<NymTopology> {
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<NymTopology> {
self.get_topology().await
}
}
@@ -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
+21 -1
View File
@@ -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(),
}
}
}
@@ -267,6 +267,7 @@ impl From<TopologyV1_1_20_2> 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(),
}
}
}
@@ -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
}
+9 -4
View File
@@ -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" }
+1
View File
@@ -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;
+9 -8
View File
@@ -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"] }
@@ -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 }
@@ -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<Location>,
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<u16>,
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<f64>,
pub longitude: Option<f64>,
}
@@ -1,9 +1,9 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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 {:#?}",
+12
View File
@@ -42,6 +42,18 @@ pub(crate) struct Location {
pub(crate) longitude: Option<f64>,
}
impl From<Location> 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| {
+2 -1
View File
@@ -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;
+1 -1
View File
@@ -1,4 +1,4 @@
pub(crate) mod delegations;
pub(crate) mod econ_stats;
pub(crate) mod http;
pub(crate) mod models;
pub mod models;
+2 -36
View File
@@ -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<Location>,
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<u16>,
pub blacklisted: bool,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
pub struct SummedDelegations {
pub owner: Addr,
+1 -1
View File
@@ -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;
+1 -22
View File
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// 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<f64>,
pub(crate) longitude: Option<f64>,
}
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<Location>) -> Self {
LocationCacheItem {
+2 -2
View File
@@ -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)]
+20 -2
View File
@@ -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",
+20 -6
View File
@@ -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));
}
}
@@ -136,7 +136,6 @@ impl HostsStore {
})
.map(Host::from)
.collect();
dbg!(&hosts);
Ok(hosts)
}
}