Skip to content

Commit f45f2e3

Browse files
committed
explorer-api: decorate mix nodes with locations from the geoip service and keep mix node cache in a hash map instead of a vec
1 parent 4ac95ad commit f45f2e3

File tree

4 files changed

+146
-19
lines changed

4 files changed

+146
-19
lines changed

explorer-api/src/country_statistics/mod.rs

+20-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use reqwest::Error as ReqwestError;
55
use models::GeoLocation;
66

77
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
8+
use crate::mix_nodes::Location;
89
use crate::state::ExplorerApiStateContext;
910

1011
pub mod country_nodes_distribution;
@@ -51,17 +52,32 @@ impl CountryStatistics {
5152

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

59-
trace!(
60+
let three_letter_iso_country_code =
61+
map_2_letter_to_3_letter_country_code(&location);
62+
63+
info!(
6064
"Ip {} is located in {:#?}",
61-
bond.mix_node.host,
62-
map_2_letter_to_3_letter_country_code(&location)
65+
bond.1.bond.mix_node.host, three_letter_iso_country_code,
6366
);
6467

68+
self.state
69+
.inner
70+
.mix_nodes
71+
.set_location(
72+
&bond.1.bond.mix_node.identity_key,
73+
Location {
74+
country_name: location.country_name,
75+
two_letter_iso_country_code: location.country_code,
76+
three_letter_iso_country_code,
77+
},
78+
)
79+
.await;
80+
6581
if (i % 100) == 0 {
6682
info!(
6783
"Located {} mixnodes in {} countries",

explorer-api/src/mix_node/http.rs

+43-2
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

+66-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,33 @@
1+
use std::collections::HashMap;
12
use std::sync::Arc;
23
use std::time::{Duration, SystemTime};
34

45
use rocket::tokio::sync::RwLock;
6+
use serde::{Deserialize, Serialize};
57

68
use mixnet_contract::MixNodeBond;
79
use validator_client::Config;
810

11+
pub(crate) type LocationCache = HashMap<String, Location>;
12+
13+
#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize)]
14+
pub(crate) struct Location {
15+
pub(crate) two_letter_iso_country_code: String,
16+
pub(crate) three_letter_iso_country_code: String,
17+
pub(crate) country_name: String,
18+
}
19+
20+
#[derive(Clone, Debug)]
21+
pub(crate) struct MixNodeBondWithLocation {
22+
pub(crate) location: Option<Location>,
23+
pub(crate) bond: MixNodeBond,
24+
}
25+
926
#[derive(Clone, Debug)]
1027
pub(crate) struct MixNodesResult {
1128
pub(crate) valid_until: SystemTime,
12-
pub(crate) value: Vec<MixNodeBond>,
29+
pub(crate) value: HashMap<String, MixNodeBondWithLocation>,
30+
location_cache: LocationCache,
1331
}
1432

1533
#[derive(Clone)]
@@ -21,12 +39,42 @@ impl ThreadsafeMixNodesResult {
2139
pub(crate) fn new() -> Self {
2240
ThreadsafeMixNodesResult {
2341
inner: Arc::new(RwLock::new(MixNodesResult {
24-
value: vec![],
42+
value: HashMap::new(),
2543
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
44+
location_cache: LocationCache::new(),
2645
})),
2746
}
2847
}
2948

49+
pub(crate) fn attach(location_cache: LocationCache) -> Self {
50+
ThreadsafeMixNodesResult {
51+
inner: Arc::new(RwLock::new(MixNodesResult {
52+
value: HashMap::new(),
53+
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
54+
location_cache,
55+
})),
56+
}
57+
}
58+
59+
pub(crate) async fn get_location_cache(&self) -> LocationCache {
60+
self.inner.read().await.location_cache.clone()
61+
}
62+
63+
pub(crate) async fn set_location(&self, identity_key: &str, location: Location) {
64+
let mut guard = self.inner.write().await;
65+
66+
// cache the location for this mix node so that it can be used when the mix node list is refreshed
67+
guard
68+
.location_cache
69+
.insert(identity_key.to_string(), location.clone());
70+
71+
// add the location to the mix node
72+
guard
73+
.value
74+
.entry(identity_key.to_string())
75+
.and_modify(|item| item.location = Some(location));
76+
}
77+
3078
pub(crate) async fn get(&self) -> MixNodesResult {
3179
// check ttl
3280
let valid_until = self.inner.clone().read().await.valid_until;
@@ -48,9 +96,24 @@ impl ThreadsafeMixNodesResult {
4896
async fn refresh(&self) {
4997
// get mixnodes and cache the new value
5098
let value = retrieve_mixnodes().await;
99+
let location_cache = self.inner.read().await.location_cache.clone();
51100
self.inner.write().await.clone_from(&MixNodesResult {
52-
value,
101+
value: value
102+
.iter()
103+
.map(|bond| {
104+
(
105+
bond.mix_node.identity_key.to_string(),
106+
MixNodeBondWithLocation {
107+
bond: bond.clone(),
108+
location: location_cache
109+
.get(&bond.mix_node.identity_key.to_string())
110+
.cloned(), // add the location, if we've located this mix node before
111+
},
112+
)
113+
})
114+
.collect(),
53115
valid_until: SystemTime::now() + Duration::from_secs(60 * 10), // valid for 10 minutes
116+
location_cache,
54117
});
55118
}
56119
}

explorer-api/src/state.rs

+17-10
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ use chrono::{DateTime, Utc};
55
use log::info;
66
use serde::{Deserialize, Serialize};
77

8+
use mixnet_contract::MixNodeBond;
9+
810
use crate::country_statistics::country_nodes_distribution::{
911
ConcurrentCountryNodesDistribution, CountryNodesDistribution,
1012
};
1113
use crate::mix_node::models::ThreadsafeMixNodeCache;
12-
use crate::mix_nodes::ThreadsafeMixNodesResult;
14+
use crate::mix_nodes::{LocationCache, ThreadsafeMixNodesResult};
1315
use crate::ping::models::ThreadsafePingCache;
14-
use mixnet_contract::MixNodeBond;
16+
use std::error::Error;
1517

1618
// TODO: change to an environment variable with a default value
1719
const STATE_FILE: &str = "explorer-api-state.json";
@@ -30,15 +32,15 @@ impl ExplorerApiState {
3032
.get()
3133
.await
3234
.value
33-
.iter()
34-
.find(|node| node.mix_node.identity_key == pubkey)
35-
.cloned()
35+
.get(pubkey)
36+
.map(|bond| bond.bond.clone())
3637
}
3738
}
3839

3940
#[derive(Debug, Serialize, Deserialize)]
4041
pub struct ExplorerApiStateOnDisk {
4142
pub(crate) country_node_distribution: CountryNodesDistribution,
43+
pub(crate) location_cache: LocationCache,
4244
pub(crate) as_at: DateTime<Utc>,
4345
}
4446

@@ -60,16 +62,14 @@ impl ExplorerApiStateContext {
6062
let json_file = get_state_file_path();
6163
let json_file_path = Path::new(&json_file);
6264
info!("Loading state from file {:?}...", json_file);
63-
match File::open(json_file_path) {
64-
Ok(file) => {
65-
let state: ExplorerApiStateOnDisk =
66-
serde_json::from_reader(file).expect("error while reading json");
65+
match get_state_from_file(json_file_path) {
66+
Ok(state) => {
6767
info!("Loaded state from file {:?}: {:?}", json_file, state);
6868
ExplorerApiState {
6969
country_node_distribution: ConcurrentCountryNodesDistribution::attach(
7070
state.country_node_distribution,
7171
),
72-
mix_nodes: ThreadsafeMixNodesResult::new(),
72+
mix_nodes: ThreadsafeMixNodesResult::attach(state.location_cache),
7373
mix_node_cache: ThreadsafeMixNodeCache::new(),
7474
ping_cache: ThreadsafePingCache::new(),
7575
}
@@ -95,6 +95,7 @@ impl ExplorerApiStateContext {
9595
let file = File::create(json_file_path).expect("unable to create state json file");
9696
let state = ExplorerApiStateOnDisk {
9797
country_node_distribution: self.inner.country_node_distribution.get_all().await,
98+
location_cache: self.inner.mix_nodes.get_location_cache().await,
9899
as_at: Utc::now(),
99100
};
100101
serde_json::to_writer(file, &state).expect("error writing state to disk");
@@ -105,3 +106,9 @@ impl ExplorerApiStateContext {
105106
fn get_state_file_path() -> String {
106107
std::env::var("API_STATE_FILE").unwrap_or_else(|_| STATE_FILE.to_string())
107108
}
109+
110+
fn get_state_from_file(json_file_path: &Path) -> Result<ExplorerApiStateOnDisk, Box<dyn Error>> {
111+
let file = File::open(json_file_path)?;
112+
let state = serde_json::from_reader(file)?;
113+
Ok(state)
114+
}

0 commit comments

Comments
 (0)