Endpoint for rewarded set inclusion probabilities (#1042)
* Add validator-api endpoint * Add validator-client method * Make it a bit nicer * Address review comments * Cap probability at 1.
This commit is contained in:
Generated
+26
-2
@@ -208,7 +208,24 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded 0.6.1",
|
||||
"url",
|
||||
"wildmatch",
|
||||
"wildmatch 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attohttpc"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"http",
|
||||
"log",
|
||||
"native-tls",
|
||||
"openssl",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"wildmatch 2.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3861,6 +3878,7 @@ name = "nym-validator-api"
|
||||
version = "0.12.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"attohttpc 0.18.0",
|
||||
"clap",
|
||||
"coconut-interface",
|
||||
"config",
|
||||
@@ -6580,7 +6598,7 @@ version = "1.0.0-beta.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79a0579dcc6fb883fe90dd3c66d76b8b8f4a1786e1e915e314b2017a500ede09"
|
||||
dependencies = [
|
||||
"attohttpc",
|
||||
"attohttpc 0.17.0",
|
||||
"bincode",
|
||||
"cfg_aliases",
|
||||
"dirs-next",
|
||||
@@ -7768,6 +7786,12 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a"
|
||||
|
||||
[[package]]
|
||||
name = "wildmatch"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::validator_api::routes::{CORE_STATUS_COUNT, SINCE_ARG};
|
||||
use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse};
|
||||
use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
use validator_api_requests::models::{
|
||||
CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse,
|
||||
@@ -101,6 +102,23 @@ impl Client {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_probs_mixnode_rewarded(
|
||||
&self,
|
||||
mixnode_id: &str,
|
||||
) -> Result<HashMap<String, f32>, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::MIXNODES,
|
||||
routes::REWARDED,
|
||||
routes::INCLUSION_CHANCE,
|
||||
&mixnode_id.to_string(),
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_gateway_core_status_count(
|
||||
&self,
|
||||
identity: IdentityKeyRef<'_>,
|
||||
|
||||
@@ -23,3 +23,4 @@ pub const SINCE_ARG: &str = "since";
|
||||
pub const STATUS: &str = "status";
|
||||
pub const REWARD_ESTIMATION: &str = "reward-estimation";
|
||||
pub const STAKE_SATURATION: &str = "stake-saturation";
|
||||
pub const INCLUSION_CHANCE: &str = "inclusion-probability";
|
||||
|
||||
@@ -62,4 +62,7 @@ coconut = ["coconut-interface", "credentials", "gateway-client/coconut"]
|
||||
[build-dependencies]
|
||||
tokio = { version = "1.4", features = ["rt-multi-thread", "macros"] }
|
||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
vergen = { version = "5", default-features = false, features = ["build", "git", "rustc", "cargo"] }
|
||||
vergen = { version = "5", default-features = false, features = ["build", "git", "rustc", "cargo"] }
|
||||
|
||||
[dev-dependencies]
|
||||
attohttpc = {version = "0.18.0", features = ["json"]}
|
||||
Vendored
+198
-14
@@ -14,7 +14,9 @@ use rand::prelude::SliceRandom;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use rocket::fairing::AdHoc;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -26,6 +28,22 @@ use validator_client::nymd::CosmWasmClient;
|
||||
|
||||
pub(crate) mod routes;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct InclusionProbabilityResponse {
|
||||
in_active: f32,
|
||||
in_reserve: f32,
|
||||
}
|
||||
|
||||
impl fmt::Display for InclusionProbabilityResponse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"in_active: {:.5}, in_reserve: {:.5}",
|
||||
self.in_active, self.in_reserve
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ValidatorCacheRefresher<C> {
|
||||
nymd_client: Client<C>,
|
||||
cache: ValidatorCache,
|
||||
@@ -178,6 +196,7 @@ impl ValidatorCache {
|
||||
routes::get_gateways,
|
||||
routes::get_active_mixnodes,
|
||||
routes::get_rewarded_mixnodes,
|
||||
routes::get_probs_mixnode_rewarded
|
||||
],
|
||||
)
|
||||
})
|
||||
@@ -214,31 +233,152 @@ impl ValidatorCache {
|
||||
// seed our rng with the hash of the block of the most recent rewarding interval
|
||||
let mut rng = ChaCha20Rng::from_seed(block_hash.unwrap());
|
||||
|
||||
// generate list of mixnodes and their relatively weight (by total stake)
|
||||
let choices = mixnodes
|
||||
.iter()
|
||||
.map(|mix| {
|
||||
// note that the theoretical maximum possible stake is equal to the total
|
||||
// supply of all tokens, i.e. 1B (which is 1 quadrillion of native tokens, i.e. 10^15 ~ 2^50)
|
||||
// which is way below maximum value of f64, so the cast is fine
|
||||
let total_stake = mix.total_stake().unwrap_or_default() as f64;
|
||||
(mix, total_stake)
|
||||
}) // if for some reason node is invalid, treat it as 0 stake/weight
|
||||
.collect::<Vec<_>>();
|
||||
self.stake_weighted_choice(mixnodes, nodes_to_select as usize, &mut rng)
|
||||
}
|
||||
|
||||
fn stake_weighted_choice(
|
||||
&self,
|
||||
mixnodes: &[MixNodeBond],
|
||||
nodes_to_select: usize,
|
||||
rng: &mut ChaCha20Rng,
|
||||
) -> Vec<MixNodeBond> {
|
||||
// generate list of mixnodes and their relatively weight (by total stake)
|
||||
let choices = self.generate_mixnode_stake_tuples(mixnodes);
|
||||
// the unwrap here is fine as an error can only be thrown under one of the following conditions:
|
||||
// - our mixnode list is empty - we have already checked for that
|
||||
// - we have invalid weights, i.e. less than zero or NaNs - it shouldn't happen in our case as we safely cast down from u128
|
||||
// - all weights are zero - it's impossible in our case as the list of nodes is not empty and weight is proportional to stake. You must have non-zero stake in order to bond
|
||||
// - we have more than u32::MAX values (which is incredibly unrealistic to have 4B mixnodes bonded... literally every other person on the planet would need one)
|
||||
choices
|
||||
.choose_multiple_weighted(&mut rng, nodes_to_select as usize, |item| item.1)
|
||||
.choose_multiple_weighted(rng, nodes_to_select, |item| item.1)
|
||||
.unwrap()
|
||||
.map(|(bond, _weight)| *bond)
|
||||
.map(|(bond, _weight)| bond)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn generate_mixnode_stake_tuples(&self, mixnodes: &[MixNodeBond]) -> Vec<(MixNodeBond, f64)> {
|
||||
mixnodes
|
||||
.iter()
|
||||
.map(|mix| {
|
||||
// note that the theoretical maximum possible stake is equal to the total
|
||||
// supply of all tokens, i.e. 1B (which is 1 quadrillion of native tokens, i.e. 10^15 ~ 2^50)
|
||||
// which is way below maximum value of f64, so the cast is fine
|
||||
let total_stake = mix.total_stake().unwrap_or_default() as f64;
|
||||
(mix.clone(), total_stake)
|
||||
}) // if for some reason node is invalid, treat it as 0 stake/weight
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Estimate probability that a node will end up in the rewarded set, by running the selection process 100 times and aggregating the results.
|
||||
// If a node is in the active set it is not counted as being in the reserve set, the probabilities are exclusive. Cumulative probabilitiy
|
||||
// can be obtained by summing the two probabilities returned.
|
||||
async fn probs_mixnode_rewarded_calculate(
|
||||
&self,
|
||||
target_mixnode_id: IdentityKey,
|
||||
mixnodes: Option<Vec<MixNodeBond>>,
|
||||
) -> Option<InclusionProbabilityResponse> {
|
||||
let mixnodes = if let Some(nodes) = mixnodes {
|
||||
nodes
|
||||
} else {
|
||||
self.inner.mixnodes.read().await.value.clone()
|
||||
};
|
||||
let total_bonded_tokens = mixnodes
|
||||
.iter()
|
||||
.fold(0u128, |acc, x| acc + x.total_stake().unwrap_or_default())
|
||||
as f64;
|
||||
let target_mixnode = mixnodes
|
||||
.iter()
|
||||
.find(|x| x.identity() == &target_mixnode_id)?;
|
||||
let rewarded_set_size = self
|
||||
.inner
|
||||
.current_mixnode_rewarded_set_size
|
||||
.load(Ordering::SeqCst) as f64;
|
||||
let active_set_size = self
|
||||
.inner
|
||||
.current_mixnode_active_set_size
|
||||
.load(Ordering::SeqCst) as f64;
|
||||
|
||||
// For running comparison tests below, needs improvement
|
||||
// let rewarded_set_size = 720.;
|
||||
// let active_set_size = 300.;
|
||||
|
||||
let prob_one_draw =
|
||||
target_mixnode.total_stake().unwrap_or_default() as f64 / total_bonded_tokens;
|
||||
// Chance to be selected in any draw for active set
|
||||
let prob_active_set = active_set_size * prob_one_draw;
|
||||
// This is likely slightly too high, as we're not correcting form them not being selected in active, should be chance to be selected, minus the chance for being not selected in reserve
|
||||
let prob_reserve_set = (rewarded_set_size - active_set_size) * prob_one_draw;
|
||||
// (rewarded_set_size - active_set_size) * prob_one_draw * (1. - prob_active_set);
|
||||
|
||||
Some(InclusionProbabilityResponse {
|
||||
in_active: if prob_active_set > 1. {
|
||||
1.
|
||||
} else {
|
||||
prob_active_set
|
||||
} as f32,
|
||||
in_reserve: if prob_reserve_set > 1. {
|
||||
1.
|
||||
} else {
|
||||
prob_reserve_set
|
||||
} as f32,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn probs_mixnode_rewarded_simulate(
|
||||
&self,
|
||||
target_mixnode_id: IdentityKey,
|
||||
mixnodes: Option<Vec<MixNodeBond>>,
|
||||
) -> Option<InclusionProbabilityResponse> {
|
||||
let mut in_active = 0;
|
||||
let mut in_reserve = 0;
|
||||
let mixnodes = if let Some(nodes) = mixnodes {
|
||||
nodes
|
||||
} else {
|
||||
self.inner.mixnodes.read().await.value.clone()
|
||||
};
|
||||
let rewarded_set_size = self
|
||||
.inner
|
||||
.current_mixnode_rewarded_set_size
|
||||
.load(Ordering::SeqCst) as usize;
|
||||
let active_set_size = self
|
||||
.inner
|
||||
.current_mixnode_active_set_size
|
||||
.load(Ordering::SeqCst) as usize;
|
||||
let mut rng = ChaCha20Rng::from_entropy();
|
||||
|
||||
// For running comparison tests below, needs improvement
|
||||
// let rewarded_set_size = 720.;
|
||||
// let active_set_size = 300.;
|
||||
|
||||
let it = 100;
|
||||
|
||||
for _ in 0..it {
|
||||
let mut rewarded_set =
|
||||
self.stake_weighted_choice(&mixnodes, rewarded_set_size, &mut rng);
|
||||
let reserve_set = rewarded_set
|
||||
.split_off(active_set_size)
|
||||
.iter()
|
||||
.map(|bond| bond.identity().clone())
|
||||
.collect::<HashSet<IdentityKey>>();
|
||||
let active_set = rewarded_set
|
||||
.iter()
|
||||
.map(|bond| bond.identity().clone())
|
||||
.collect::<HashSet<IdentityKey>>();
|
||||
|
||||
if active_set.contains(&target_mixnode_id) {
|
||||
in_active += 1;
|
||||
} else if reserve_set.contains(&target_mixnode_id) {
|
||||
in_reserve += 1;
|
||||
}
|
||||
}
|
||||
Some(InclusionProbabilityResponse {
|
||||
in_active: in_active as f32 / it as f32,
|
||||
in_reserve: in_reserve as f32 / it as f32,
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_cache(
|
||||
&self,
|
||||
mixnodes: Vec<MixNodeBond>,
|
||||
@@ -415,3 +555,47 @@ impl ValidatorCacheInner {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod test {
|
||||
// use crate::cache::InclusionProbabilityResponse;
|
||||
|
||||
// use super::ValidatorCache;
|
||||
// use mixnet_contract_common::MixNodeBond;
|
||||
|
||||
// #[tokio::test]
|
||||
// async fn test_inclusion_probabilities() {
|
||||
// let cache = ValidatorCache::new();
|
||||
// let response = attohttpc::get("https://sandbox-validator.nymtech.net/api/v1/mixnodes")
|
||||
// .send()
|
||||
// .unwrap();
|
||||
// let mixnodes: Vec<MixNodeBond> = response.json().unwrap();
|
||||
// let calculated = cache
|
||||
// .probs_mixnode_rewarded_calculate(
|
||||
// "ysmgeYJQPBFzB2TgqBjN5BE3Rb79CgFANrnJaYd8woQ".to_string(),
|
||||
// Some(mixnodes.clone()),
|
||||
// )
|
||||
// .await
|
||||
// .unwrap();
|
||||
// let mut simulated_avg = Vec::new();
|
||||
// for _ in 0..1000 {
|
||||
// let simulated = cache
|
||||
// .probs_mixnode_rewarded_simulate(
|
||||
// "ysmgeYJQPBFzB2TgqBjN5BE3Rb79CgFANrnJaYd8woQ".to_string(),
|
||||
// Some(mixnodes.clone()),
|
||||
// )
|
||||
// .await
|
||||
// .unwrap();
|
||||
// simulated_avg.push(simulated);
|
||||
// }
|
||||
// let simulated = simulated_avg.iter().fold((0., 0.), |acc, x| {
|
||||
// (acc.0 + x.in_active, acc.1 + x.in_reserve)
|
||||
// });
|
||||
// let simulated = InclusionProbabilityResponse {
|
||||
// in_active: simulated.0 / 100.,
|
||||
// in_reserve: simulated.1 / 100.,
|
||||
// };
|
||||
// println!("calculted: {calculated}");
|
||||
// println!("simulated: {simulated}");
|
||||
// }
|
||||
// }
|
||||
|
||||
Vendored
+13
@@ -1,6 +1,7 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::cache::InclusionProbabilityResponse;
|
||||
use crate::cache::ValidatorCache;
|
||||
use mixnet_contract_common::{GatewayBond, MixNodeBond};
|
||||
use rocket::serde::json::Json;
|
||||
@@ -25,3 +26,15 @@ pub(crate) async fn get_rewarded_mixnodes(cache: &State<ValidatorCache>) -> Json
|
||||
pub(crate) async fn get_active_mixnodes(cache: &State<ValidatorCache>) -> Json<Vec<MixNodeBond>> {
|
||||
Json(cache.active_mixnodes().await.value)
|
||||
}
|
||||
|
||||
#[get("/mixnodes/rewarded/inclusion-probability/<mixnode_id>")]
|
||||
pub(crate) async fn get_probs_mixnode_rewarded(
|
||||
cache: &State<ValidatorCache>,
|
||||
mixnode_id: String,
|
||||
) -> Json<Option<InclusionProbabilityResponse>> {
|
||||
Json(
|
||||
cache
|
||||
.probs_mixnode_rewarded_calculate(mixnode_id, None)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user