Skip to content

Explorer API: add new HTTP resource to decorate mix nodes with geoip locations #734

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 11, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions explorer-api/src/country_statistics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use reqwest::Error as ReqwestError;
use models::GeoLocation;

use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
use crate::mix_nodes::Location;
use crate::state::ExplorerApiStateContext;

pub mod country_nodes_distribution;
Expand Down Expand Up @@ -51,17 +52,35 @@ impl CountryStatistics {

info!("Locating mixnodes...");
for (i, bond) in mixnode_bonds.iter().enumerate() {
match locate(&bond.mix_node.host).await {
match locate(&bond.1.bond.mix_node.host).await {
Ok(location) => {
let country_code = map_2_letter_to_3_letter_country_code(&location);
*(distribution.entry(country_code)).or_insert(0) += 1;

let three_letter_iso_country_code =
map_2_letter_to_3_letter_country_code(&location);

trace!(
"Ip {} is located in {:#?}",
bond.mix_node.host,
map_2_letter_to_3_letter_country_code(&location)
bond.1.bond.mix_node.host,
three_letter_iso_country_code,
);

self.state
.inner
.mix_nodes
.set_location(
&bond.1.bond.mix_node.identity_key,
Location {
country_name: location.country_name,
two_letter_iso_country_code: location.country_code,
three_letter_iso_country_code,
lat: location.latitude,
lng: location.longitude,
},
)
.await;

if (i % 100) == 0 {
info!(
"Located {} mixnodes in {} countries",
Expand Down
45 changes: 43 additions & 2 deletions explorer-api/src/mix_node/http.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,54 @@
use reqwest::Error as ReqwestError;

use rocket::serde::json::Json;
use rocket::{Route, State};
use serde::Serialize;

use mixnet_contract::{Addr, Coin, Layer, MixNode};

use crate::mix_node::models::{NodeDescription, NodeStats};
use crate::mix_nodes::Location;
use crate::state::ExplorerApiStateContext;

pub fn mix_node_make_default_routes() -> Vec<Route> {
routes_with_openapi![get_description, get_stats]
routes_with_openapi![get_description, get_stats, list]
}

#[derive(Clone, Debug, Serialize, JsonSchema)]
pub(crate) struct PrettyMixNodeBondWithLocation {
pub location: Option<Location>,
pub bond_amount: Coin,
pub total_delegation: Coin,
pub owner: Addr,
pub layer: Layer,
pub mix_node: MixNode,
}

#[openapi(tag = "mix_node")]
#[get("/")]
pub(crate) async fn list(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyMixNodeBondWithLocation>> {
Json(
state
.inner
.mix_nodes
.get()
.await
.value
.values()
.map(|i| {
let mix_node = i.bond.clone();
PrettyMixNodeBondWithLocation {
location: i.location.clone(),
bond_amount: mix_node.bond_amount,
total_delegation: mix_node.total_delegation,
owner: mix_node.owner,
layer: mix_node.layer,
mix_node: mix_node.mix_node,
}
})
.collect::<Vec<PrettyMixNodeBondWithLocation>>(),
)
}

#[openapi(tag = "mix_node")]
Expand Down
71 changes: 68 additions & 3 deletions explorer-api/src/mix_nodes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,35 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime};

use rocket::tokio::sync::RwLock;
use serde::{Deserialize, Serialize};

use mixnet_contract::MixNodeBond;
use validator_client::Config;

pub(crate) type LocationCache = HashMap<String, Location>;

#[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) lat: f32,
pub(crate) lng: f32,
}

#[derive(Clone, Debug)]
pub(crate) struct MixNodeBondWithLocation {
pub(crate) location: Option<Location>,
pub(crate) bond: MixNodeBond,
}

#[derive(Clone, Debug)]
pub(crate) struct MixNodesResult {
pub(crate) valid_until: SystemTime,
pub(crate) value: Vec<MixNodeBond>,
pub(crate) value: HashMap<String, MixNodeBondWithLocation>,
location_cache: LocationCache,
}

#[derive(Clone)]
Expand All @@ -21,12 +41,42 @@ impl ThreadsafeMixNodesResult {
pub(crate) fn new() -> Self {
ThreadsafeMixNodesResult {
inner: Arc::new(RwLock::new(MixNodesResult {
value: vec![],
value: HashMap::new(),
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
location_cache: LocationCache::new(),
})),
}
}

pub(crate) fn attach(location_cache: LocationCache) -> Self {
ThreadsafeMixNodesResult {
inner: Arc::new(RwLock::new(MixNodesResult {
value: HashMap::new(),
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
location_cache,
})),
}
}

pub(crate) async fn get_location_cache(&self) -> LocationCache {
self.inner.read().await.location_cache.clone()
}

pub(crate) async fn set_location(&self, identity_key: &str, location: Location) {
let mut guard = self.inner.write().await;

// cache the location for this mix node so that it can be used when the mix node list is refreshed
guard
.location_cache
.insert(identity_key.to_string(), location.clone());

// add the location to the mix node
guard
.value
.entry(identity_key.to_string())
.and_modify(|item| item.location = Some(location));
}

pub(crate) async fn get(&self) -> MixNodesResult {
// check ttl
let valid_until = self.inner.clone().read().await.valid_until;
Expand All @@ -48,9 +98,24 @@ impl ThreadsafeMixNodesResult {
async fn refresh(&self) {
// get mixnodes and cache the new value
let value = retrieve_mixnodes().await;
let location_cache = self.inner.read().await.location_cache.clone();
self.inner.write().await.clone_from(&MixNodesResult {
value,
value: value
.iter()
.map(|bond| {
(
bond.mix_node.identity_key.to_string(),
MixNodeBondWithLocation {
bond: bond.clone(),
location: location_cache
.get(&bond.mix_node.identity_key.to_string())
.cloned(), // add the location, if we've located this mix node before
},
)
})
.collect(),
valid_until: SystemTime::now() + Duration::from_secs(60 * 10), // valid for 10 minutes
location_cache,
});
}
}
Expand Down
27 changes: 17 additions & 10 deletions explorer-api/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ use chrono::{DateTime, Utc};
use log::info;
use serde::{Deserialize, Serialize};

use mixnet_contract::MixNodeBond;

use crate::country_statistics::country_nodes_distribution::{
ConcurrentCountryNodesDistribution, CountryNodesDistribution,
};
use crate::mix_node::models::ThreadsafeMixNodeCache;
use crate::mix_nodes::ThreadsafeMixNodesResult;
use crate::mix_nodes::{LocationCache, ThreadsafeMixNodesResult};
use crate::ping::models::ThreadsafePingCache;
use mixnet_contract::MixNodeBond;
use std::error::Error;

// TODO: change to an environment variable with a default value
const STATE_FILE: &str = "explorer-api-state.json";
Expand All @@ -30,15 +32,15 @@ impl ExplorerApiState {
.get()
.await
.value
.iter()
.find(|node| node.mix_node.identity_key == pubkey)
.cloned()
.get(pubkey)
.map(|bond| bond.bond.clone())
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ExplorerApiStateOnDisk {
pub(crate) country_node_distribution: CountryNodesDistribution,
pub(crate) location_cache: LocationCache,
pub(crate) as_at: DateTime<Utc>,
}

Expand All @@ -60,16 +62,14 @@ impl ExplorerApiStateContext {
let json_file = get_state_file_path();
let json_file_path = Path::new(&json_file);
info!("Loading state from file {:?}...", json_file);
match File::open(json_file_path) {
Ok(file) => {
let state: ExplorerApiStateOnDisk =
serde_json::from_reader(file).expect("error while reading json");
match get_state_from_file(json_file_path) {
Ok(state) => {
info!("Loaded state from file {:?}: {:?}", json_file, state);
ExplorerApiState {
country_node_distribution: ConcurrentCountryNodesDistribution::attach(
state.country_node_distribution,
),
mix_nodes: ThreadsafeMixNodesResult::new(),
mix_nodes: ThreadsafeMixNodesResult::attach(state.location_cache),
mix_node_cache: ThreadsafeMixNodeCache::new(),
ping_cache: ThreadsafePingCache::new(),
}
Expand All @@ -95,6 +95,7 @@ impl ExplorerApiStateContext {
let file = File::create(json_file_path).expect("unable to create state json file");
let state = ExplorerApiStateOnDisk {
country_node_distribution: self.inner.country_node_distribution.get_all().await,
location_cache: self.inner.mix_nodes.get_location_cache().await,
as_at: Utc::now(),
};
serde_json::to_writer(file, &state).expect("error writing state to disk");
Expand All @@ -105,3 +106,9 @@ impl ExplorerApiStateContext {
fn get_state_file_path() -> String {
std::env::var("API_STATE_FILE").unwrap_or_else(|_| STATE_FILE.to_string())
}

fn get_state_from_file(json_file_path: &Path) -> Result<ExplorerApiStateOnDisk, Box<dyn Error>> {
let file = File::open(json_file_path)?;
let state = serde_json::from_reader(file)?;
Ok(state)
}