Skip to content

Commit ccf8063

Browse files
authored
Explorer API: add new HTTP resource to decorate mix nodes with geoip locations (#734)
* explorer-api: decorate mix nodes with locations from the geoip service and keep mix node cache in a hash map instead of a vec * explorer-api: add `lat` and `lng` for map views * explorer-api: remove function and simplify code * explorer-api: review feedback * network-explorer: format
1 parent 8694396 commit ccf8063

File tree

7 files changed

+195
-71
lines changed

7 files changed

+195
-71
lines changed

explorer-api/src/country_statistics/country_nodes_distribution.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ impl ConcurrentCountryNodesDistribution {
1616
}
1717
}
1818

19-
pub(crate) fn attach(country_node_distribution: CountryNodesDistribution) -> Self {
19+
pub(crate) fn new_from_distribution(
20+
country_node_distribution: CountryNodesDistribution,
21+
) -> Self {
2022
ConcurrentCountryNodesDistribution {
2123
inner: Arc::new(RwLock::new(country_node_distribution)),
2224
}

explorer-api/src/country_statistics/mod.rs

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
use isocountry::CountryCode;
21
use log::{info, trace, warn};
32
use reqwest::Error as ReqwestError;
43

5-
use models::GeoLocation;
6-
74
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
5+
use crate::mix_nodes::{GeoLocation, Location};
86
use crate::state::ExplorerApiStateContext;
97

108
pub mod country_nodes_distribution;
119
pub mod http;
12-
mod models;
1310

1411
pub(crate) struct CountryStatistics {
1512
state: ExplorerApiStateContext,
@@ -38,30 +35,31 @@ impl CountryStatistics {
3835
/// Retrieves the current list of mixnodes from the validators and calculates how many nodes are in each country
3936
async fn calculate_nodes_per_country(&mut self) {
4037
// force the mixnode cache to invalidate
41-
let mixnode_bonds = self
42-
.state
43-
.inner
44-
.mix_nodes
45-
.clone()
46-
.refresh_and_get()
47-
.await
48-
.value;
38+
let mixnode_bonds = self.state.inner.mix_nodes.refresh_and_get().await.value;
4939

5040
let mut distribution = CountryNodesDistribution::new();
5141

5242
info!("Locating mixnodes...");
53-
for (i, bond) in mixnode_bonds.iter().enumerate() {
54-
match locate(&bond.mix_node.host).await {
55-
Ok(location) => {
56-
let country_code = map_2_letter_to_3_letter_country_code(&location);
57-
*(distribution.entry(country_code)).or_insert(0) += 1;
43+
for (i, cache_item) in mixnode_bonds.values().enumerate() {
44+
match locate(&cache_item.bond.mix_node.host).await {
45+
Ok(geo_location) => {
46+
let location = Location::new(geo_location);
47+
48+
*(distribution.entry(location.three_letter_iso_country_code.to_string()))
49+
.or_insert(0) += 1;
5850

5951
trace!(
6052
"Ip {} is located in {:#?}",
61-
bond.mix_node.host,
62-
map_2_letter_to_3_letter_country_code(&location)
53+
cache_item.bond.mix_node.host,
54+
location.three_letter_iso_country_code,
6355
);
6456

57+
self.state
58+
.inner
59+
.mix_nodes
60+
.set_location(&cache_item.bond.mix_node.identity_key, location)
61+
.await;
62+
6563
if (i % 100) == 0 {
6664
info!(
6765
"Located {} mixnodes in {} countries",
@@ -91,19 +89,6 @@ impl CountryStatistics {
9189
}
9290
}
9391

94-
fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String {
95-
match CountryCode::for_alpha2(&geo.country_code) {
96-
Ok(three_letter_country_code) => three_letter_country_code.alpha3().to_string(),
97-
Err(_e) => {
98-
warn!(
99-
"❌ Oh no! map_2_letter_to_3_letter_country_code failed for '{:#?}'",
100-
geo
101-
);
102-
"???".to_string()
103-
}
104-
}
105-
}
106-
10792
async fn locate(ip: &str) -> Result<GeoLocation, ReqwestError> {
10893
let response = reqwest::get(format!("{}{}", crate::GEO_IP_SERVICE, ip)).await?;
10994
let location = response.json::<GeoLocation>().await?;

explorer-api/src/country_statistics/models.rs

Lines changed: 0 additions & 16 deletions
This file was deleted.

explorer-api/src/mix_node/http.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,54 @@
11
use reqwest::Error as ReqwestError;
2-
32
use rocket::serde::json::Json;
43
use rocket::{Route, State};
4+
use serde::Serialize;
5+
6+
use mixnet_contract::{Addr, Coin, Layer, MixNode};
57

68
use crate::mix_node::models::{NodeDescription, NodeStats};
9+
use crate::mix_nodes::Location;
710
use crate::state::ExplorerApiStateContext;
811

912
pub fn mix_node_make_default_routes() -> Vec<Route> {
10-
routes_with_openapi![get_description, get_stats]
13+
routes_with_openapi![get_description, get_stats, list]
14+
}
15+
16+
#[derive(Clone, Debug, Serialize, JsonSchema)]
17+
pub(crate) struct PrettyMixNodeBondWithLocation {
18+
pub location: Option<Location>,
19+
pub bond_amount: Coin,
20+
pub total_delegation: Coin,
21+
pub owner: Addr,
22+
pub layer: Layer,
23+
pub mix_node: MixNode,
24+
}
25+
26+
#[openapi(tag = "mix_node")]
27+
#[get("/")]
28+
pub(crate) async fn list(
29+
state: &State<ExplorerApiStateContext>,
30+
) -> Json<Vec<PrettyMixNodeBondWithLocation>> {
31+
Json(
32+
state
33+
.inner
34+
.mix_nodes
35+
.get()
36+
.await
37+
.value
38+
.values()
39+
.map(|i| {
40+
let mix_node = i.bond.clone();
41+
PrettyMixNodeBondWithLocation {
42+
location: i.location.clone(),
43+
bond_amount: mix_node.bond_amount,
44+
total_delegation: mix_node.total_delegation,
45+
owner: mix_node.owner,
46+
layer: mix_node.layer,
47+
mix_node: mix_node.mix_node,
48+
}
49+
})
50+
.collect::<Vec<PrettyMixNodeBondWithLocation>>(),
51+
)
1152
}
1253

1354
#[openapi(tag = "mix_node")]

explorer-api/src/mix_nodes/mod.rs

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,66 @@
1+
mod utils;
2+
3+
use std::collections::HashMap;
14
use std::sync::Arc;
25
use std::time::{Duration, SystemTime};
36

47
use rocket::tokio::sync::RwLock;
8+
use serde::{Deserialize, Serialize};
59

10+
use crate::mix_nodes::utils::map_2_letter_to_3_letter_country_code;
611
use mixnet_contract::MixNodeBond;
712
use validator_client::Config;
813

14+
pub(crate) type LocationCache = HashMap<String, Location>;
15+
16+
#[derive(Debug, Deserialize)]
17+
pub(crate) struct GeoLocation {
18+
pub(crate) ip: String,
19+
pub(crate) country_code: String,
20+
pub(crate) country_name: String,
21+
pub(crate) region_code: String,
22+
pub(crate) region_name: String,
23+
pub(crate) city: String,
24+
pub(crate) zip_code: String,
25+
pub(crate) time_zone: String,
26+
pub(crate) latitude: f32,
27+
pub(crate) longitude: f32,
28+
pub(crate) metro_code: u32,
29+
}
30+
31+
#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize)]
32+
pub(crate) struct Location {
33+
pub(crate) two_letter_iso_country_code: String,
34+
pub(crate) three_letter_iso_country_code: String,
35+
pub(crate) country_name: String,
36+
pub(crate) lat: f32,
37+
pub(crate) lng: f32,
38+
}
39+
40+
impl Location {
41+
pub(crate) fn new(geo_location: GeoLocation) -> Self {
42+
let three_letter_iso_country_code = map_2_letter_to_3_letter_country_code(&geo_location);
43+
Location {
44+
country_name: geo_location.country_name,
45+
two_letter_iso_country_code: geo_location.country_code,
46+
three_letter_iso_country_code,
47+
lat: geo_location.latitude,
48+
lng: geo_location.longitude,
49+
}
50+
}
51+
}
52+
53+
#[derive(Clone, Debug)]
54+
pub(crate) struct MixNodeBondWithLocation {
55+
pub(crate) location: Option<Location>,
56+
pub(crate) bond: MixNodeBond,
57+
}
58+
959
#[derive(Clone, Debug)]
1060
pub(crate) struct MixNodesResult {
1161
pub(crate) valid_until: SystemTime,
12-
pub(crate) value: Vec<MixNodeBond>,
62+
pub(crate) value: HashMap<String, MixNodeBondWithLocation>,
63+
location_cache: LocationCache,
1364
}
1465

1566
#[derive(Clone)]
@@ -21,23 +72,53 @@ impl ThreadsafeMixNodesResult {
2172
pub(crate) fn new() -> Self {
2273
ThreadsafeMixNodesResult {
2374
inner: Arc::new(RwLock::new(MixNodesResult {
24-
value: vec![],
75+
value: HashMap::new(),
2576
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
77+
location_cache: LocationCache::new(),
2678
})),
2779
}
2880
}
2981

82+
pub(crate) fn new_with_location_cache(location_cache: LocationCache) -> Self {
83+
ThreadsafeMixNodesResult {
84+
inner: Arc::new(RwLock::new(MixNodesResult {
85+
value: HashMap::new(),
86+
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
87+
location_cache,
88+
})),
89+
}
90+
}
91+
92+
pub(crate) async fn get_location_cache(&self) -> LocationCache {
93+
self.inner.read().await.location_cache.clone()
94+
}
95+
96+
pub(crate) async fn set_location(&self, identity_key: &str, location: Location) {
97+
let mut guard = self.inner.write().await;
98+
99+
// cache the location for this mix node so that it can be used when the mix node list is refreshed
100+
guard
101+
.location_cache
102+
.insert(identity_key.to_string(), location.clone());
103+
104+
// add the location to the mix node
105+
guard
106+
.value
107+
.entry(identity_key.to_string())
108+
.and_modify(|item| item.location = Some(location));
109+
}
110+
30111
pub(crate) async fn get(&self) -> MixNodesResult {
31112
// check ttl
32-
let valid_until = self.inner.clone().read().await.valid_until;
113+
let valid_until = self.inner.read().await.valid_until;
33114

34115
if valid_until < SystemTime::now() {
35116
// force reload
36117
self.refresh().await;
37118
}
38119

39120
// return in-memory cache
40-
self.inner.clone().read().await.clone()
121+
self.inner.read().await.clone()
41122
}
42123

43124
pub(crate) async fn refresh_and_get(&self) -> MixNodesResult {
@@ -48,10 +129,21 @@ impl ThreadsafeMixNodesResult {
48129
async fn refresh(&self) {
49130
// get mixnodes and cache the new value
50131
let value = retrieve_mixnodes().await;
51-
self.inner.write().await.clone_from(&MixNodesResult {
52-
value,
132+
let location_cache = self.inner.read().await.location_cache.clone();
133+
*self.inner.write().await = MixNodesResult {
134+
value: value
135+
.into_iter()
136+
.map(|bond| {
137+
let location = location_cache.get(&bond.mix_node.identity_key).cloned(); // add the location, if we've located this mix node before
138+
(
139+
bond.mix_node.identity_key.to_string(),
140+
MixNodeBondWithLocation { bond, location },
141+
)
142+
})
143+
.collect(),
53144
valid_until: SystemTime::now() + Duration::from_secs(60 * 10), // valid for 10 minutes
54-
});
145+
location_cache,
146+
};
55147
}
56148
}
57149

explorer-api/src/mix_nodes/utils.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use crate::mix_nodes::GeoLocation;
2+
use isocountry::CountryCode;
3+
4+
pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String {
5+
match CountryCode::for_alpha2(&geo.country_code) {
6+
Ok(three_letter_country_code) => three_letter_country_code.alpha3().to_string(),
7+
Err(_e) => {
8+
warn!(
9+
"❌ Oh no! map_2_letter_to_3_letter_country_code failed for '{:#?}'",
10+
geo
11+
);
12+
"???".to_string()
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)