d8c84cc4d6
* wip * wip: wrap node's sphinx key with a manager * wip: choosing correct key for packet processing * further propagation of key rotation information * attaching key rotation information to reply surbs * added basic key rotation information to mixnet contract * wip: introducing cached queries for key rotation info from nym api * unified nym-api contract cache refreshing * finish packet decoding * multi api client + retrieving rotation id * rotating sphinx key files * logic for migrating config file * wip: putting new sphinx keys to self described endpoints * processing loop of KeyRotationController * fixed sphinx key loading * rotating bloomfilters * wired up KeyRotationController * flushing bloomfilters to disk and loading * most of nym-node changes * post rebase fixes * fixes due to backwards compatible hostkeys * split http state.rs file * dont use deprecated fields * fixed backwards compatible deserialisation of host information * split up node describe cache * added a dedicated CacheRefresher listener to perform full refresh outside the set interval * controlling announced sphinx keys within nym-api * retrieving rotation id when pulling topology * split nym-nodes http handlers * v2 nym-api endpoints to retrieve nodes with additional metadata information * bug fixes... * additional bugfixes and guards against stuck epoch * testnet manager: set first nym-api as the rewarder * fixed host information deserialisation * fixed panic during first key rotation * post rebase fixes * clippy * more guards against stuck epochs * added helper method to reset node's sphinx key * instantiate mixnet contract with custom key rotation validity * additional bugfixes and debugging nym-api deadlock * passing shutdown to nym apis client * remove dead test * post rebasing fixes * missing MixnetQueryClient variants * remove usage of deprecated methods in sdk example * fix: incorrect method signature * post rebasing fixes * attempt to retrieve key rotation id before doing any config migration work * ignore tests relying on networking behaviour * allow networking failures in certain tests
386 lines
12 KiB
Rust
386 lines
12 KiB
Rust
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
use crate::node_describe_cache::cache::DescribedNodes;
|
|
use crate::node_status_api::helpers::RewardedSetStatus;
|
|
use crate::node_status_api::models::Uptime;
|
|
use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate};
|
|
use crate::nym_contract_cache::cache::data::ConfigScoreData;
|
|
use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname;
|
|
use crate::support::storage::NymApiStorage;
|
|
use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer};
|
|
use nym_api_requests::models::{
|
|
ConfigScore, DescribedNodeType, DetailedNodePerformance, GatewayBondAnnotated,
|
|
MixNodeBondAnnotated, NodeAnnotation, NodePerformance, NymNodeDescription, RoutingScore,
|
|
};
|
|
use nym_contracts_common::NaiveFloat;
|
|
use nym_mixnet_contract_common::{Interval, NodeId, VersionScoreFormulaParams};
|
|
use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams};
|
|
use nym_topology::CachedEpochRewardedSet;
|
|
use std::collections::HashMap;
|
|
use tracing::trace;
|
|
|
|
pub(super) async fn get_mixnode_reliability_from_storage(
|
|
storage: &NymApiStorage,
|
|
mix_id: NodeId,
|
|
epoch: Interval,
|
|
) -> Option<f32> {
|
|
storage
|
|
.get_average_mixnode_reliability_in_the_last_24hrs(
|
|
mix_id,
|
|
epoch.current_epoch_end_unix_timestamp(),
|
|
)
|
|
.await
|
|
.ok()
|
|
}
|
|
|
|
pub(super) async fn get_gateway_reliability_from_storage(
|
|
storage: &NymApiStorage,
|
|
node_id: NodeId,
|
|
epoch: Interval,
|
|
) -> Option<f32> {
|
|
storage
|
|
.get_average_gateway_reliability_in_the_last_24hrs(
|
|
node_id,
|
|
epoch.current_epoch_end_unix_timestamp(),
|
|
)
|
|
.await
|
|
.ok()
|
|
}
|
|
|
|
pub(super) async fn get_node_reliability_from_storage(
|
|
storage: &NymApiStorage,
|
|
node_id: NodeId,
|
|
epoch: Interval,
|
|
) -> Option<f32> {
|
|
storage
|
|
.get_average_node_reliability_in_the_last_24hrs(
|
|
node_id,
|
|
epoch.current_epoch_end_unix_timestamp(),
|
|
)
|
|
.await
|
|
.ok()
|
|
}
|
|
|
|
async fn get_routing_score(
|
|
storage: &NymApiStorage,
|
|
node_id: NodeId,
|
|
typ: DescribedNodeType,
|
|
epoch: Interval,
|
|
) -> RoutingScore {
|
|
let maybe_reliability = match typ {
|
|
DescribedNodeType::LegacyMixnode => {
|
|
get_mixnode_reliability_from_storage(storage, node_id, epoch).await
|
|
}
|
|
DescribedNodeType::LegacyGateway => {
|
|
get_gateway_reliability_from_storage(storage, node_id, epoch).await
|
|
}
|
|
DescribedNodeType::NymNode => {
|
|
get_node_reliability_from_storage(storage, node_id, epoch).await
|
|
}
|
|
};
|
|
// reliability: 0-100
|
|
// score: 0-1
|
|
let reliability = maybe_reliability.unwrap_or_default();
|
|
let score = reliability / 100.;
|
|
|
|
trace!("reliability for {node_id}: {maybe_reliability:?}. routing score: {score}");
|
|
RoutingScore::new(score as f64)
|
|
}
|
|
|
|
fn versions_behind_factor_to_config_score(
|
|
versions_behind: u32,
|
|
params: VersionScoreFormulaParams,
|
|
) -> f64 {
|
|
let penalty = params.penalty.naive_to_f64();
|
|
let scaling = params.penalty_scaling.naive_to_f64();
|
|
|
|
// version_score = penalty ^ (num_versions_behind ^ penalty_scaling)
|
|
penalty.powf((versions_behind as f64).powf(scaling))
|
|
}
|
|
|
|
fn calculate_config_score(
|
|
config_score_data: &ConfigScoreData,
|
|
described_data: Option<&NymNodeDescription>,
|
|
) -> ConfigScore {
|
|
let Some(described) = described_data else {
|
|
return ConfigScore::unavailable();
|
|
};
|
|
|
|
let node_version = &described.description.build_information.build_version;
|
|
let Ok(reported_semver) = node_version.parse::<semver::Version>() else {
|
|
return ConfigScore::bad_semver();
|
|
};
|
|
let versions_behind = config_score_data
|
|
.config_score_params
|
|
.version_weights
|
|
.versions_behind_factor(
|
|
&reported_semver,
|
|
&config_score_data.nym_node_version_history,
|
|
);
|
|
|
|
let runs_nym_node = described.description.build_information.binary_name == "nym-node";
|
|
let accepted_terms_and_conditions = described
|
|
.description
|
|
.auxiliary_details
|
|
.accepted_operator_terms_and_conditions;
|
|
|
|
let version_score = if !runs_nym_node || !accepted_terms_and_conditions {
|
|
0.
|
|
} else {
|
|
versions_behind_factor_to_config_score(
|
|
versions_behind,
|
|
config_score_data
|
|
.config_score_params
|
|
.version_score_formula_params,
|
|
)
|
|
};
|
|
|
|
ConfigScore::new(
|
|
version_score,
|
|
versions_behind,
|
|
accepted_terms_and_conditions,
|
|
runs_nym_node,
|
|
)
|
|
}
|
|
|
|
// TODO: this might have to be moved to a different file if other places also rely on this functionality
|
|
fn get_rewarded_set_status(
|
|
rewarded_set: &CachedEpochRewardedSet,
|
|
node_id: NodeId,
|
|
) -> RewardedSetStatus {
|
|
if rewarded_set.is_standby(&node_id) {
|
|
RewardedSetStatus::Standby
|
|
} else if rewarded_set.is_active_mixnode(&node_id) {
|
|
RewardedSetStatus::Active
|
|
} else {
|
|
RewardedSetStatus::Inactive
|
|
}
|
|
}
|
|
|
|
#[deprecated]
|
|
pub(super) async fn annotate_legacy_mixnodes_nodes_with_details(
|
|
storage: &NymApiStorage,
|
|
mixnodes: Vec<LegacyMixNodeDetailsWithLayer>,
|
|
interval_reward_params: RewardingParams,
|
|
current_interval: Interval,
|
|
rewarded_set: &CachedEpochRewardedSet,
|
|
) -> HashMap<NodeId, MixNodeBondAnnotated> {
|
|
let mut annotated = HashMap::new();
|
|
for mixnode in mixnodes {
|
|
let stake_saturation = mixnode
|
|
.rewarding_details
|
|
.bond_saturation(&interval_reward_params);
|
|
|
|
let uncapped_stake_saturation = mixnode
|
|
.rewarding_details
|
|
.uncapped_bond_saturation(&interval_reward_params);
|
|
|
|
let rewarded_set_status = get_rewarded_set_status(rewarded_set, mixnode.mix_id());
|
|
|
|
// If the performance can't be obtained, because the nym-api was not started with
|
|
// the monitoring (and hence, storage), then reward estimates will be all zero
|
|
let performance =
|
|
get_mixnode_reliability_from_storage(storage, mixnode.mix_id(), current_interval)
|
|
.await
|
|
.map(Uptime::new)
|
|
.map(Into::into)
|
|
.unwrap_or_default();
|
|
|
|
let reward_estimate = compute_reward_estimate(
|
|
&mixnode,
|
|
performance,
|
|
rewarded_set_status,
|
|
interval_reward_params,
|
|
current_interval,
|
|
);
|
|
|
|
let node_performance = storage
|
|
.construct_mixnode_report(mixnode.mix_id())
|
|
.await
|
|
.map(NodePerformance::from)
|
|
.ok()
|
|
.unwrap_or_default();
|
|
|
|
let Some((ip_addresses, _)) =
|
|
legacy_host_to_ips_and_hostname(&mixnode.bond_information.mix_node.host)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
let (estimated_operator_apy, estimated_delegators_apy) =
|
|
compute_apy_from_reward(&mixnode, reward_estimate, current_interval);
|
|
|
|
annotated.insert(
|
|
mixnode.mix_id(),
|
|
MixNodeBondAnnotated {
|
|
// all legacy nodes are always blacklisted
|
|
blacklisted: true,
|
|
mixnode_details: mixnode,
|
|
stake_saturation,
|
|
uncapped_stake_saturation,
|
|
performance,
|
|
node_performance,
|
|
estimated_operator_apy,
|
|
estimated_delegators_apy,
|
|
ip_addresses,
|
|
},
|
|
);
|
|
}
|
|
annotated
|
|
}
|
|
|
|
#[deprecated]
|
|
pub(crate) async fn annotate_legacy_gateways_with_details(
|
|
storage: &NymApiStorage,
|
|
gateway_bonds: Vec<LegacyGatewayBondWithId>,
|
|
current_interval: Interval,
|
|
) -> HashMap<NodeId, GatewayBondAnnotated> {
|
|
let mut annotated = HashMap::new();
|
|
for gateway_bond in gateway_bonds {
|
|
let performance =
|
|
get_gateway_reliability_from_storage(storage, gateway_bond.node_id, current_interval)
|
|
.await
|
|
.map(Uptime::new)
|
|
.map(Into::into)
|
|
.unwrap_or_default();
|
|
|
|
let node_performance = storage
|
|
.construct_gateway_report(gateway_bond.node_id)
|
|
.await
|
|
.map(NodePerformance::from)
|
|
.ok()
|
|
.unwrap_or_default();
|
|
|
|
let Some((ip_addresses, _)) =
|
|
legacy_host_to_ips_and_hostname(&gateway_bond.bond.gateway.host)
|
|
else {
|
|
continue;
|
|
};
|
|
|
|
annotated.insert(
|
|
gateway_bond.node_id,
|
|
GatewayBondAnnotated {
|
|
// all legacy nodes are always blacklisted
|
|
blacklisted: true,
|
|
gateway_bond,
|
|
self_described: None,
|
|
performance,
|
|
node_performance,
|
|
ip_addresses,
|
|
},
|
|
);
|
|
}
|
|
annotated
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) async fn produce_node_annotations(
|
|
storage: &NymApiStorage,
|
|
config_score_data: &ConfigScoreData,
|
|
legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer],
|
|
legacy_gateways: &[LegacyGatewayBondWithId],
|
|
nym_nodes: &[NymNodeDetails],
|
|
rewarded_set: &CachedEpochRewardedSet,
|
|
current_interval: Interval,
|
|
described_nodes: &DescribedNodes,
|
|
) -> HashMap<NodeId, NodeAnnotation> {
|
|
let mut annotations = HashMap::new();
|
|
|
|
for legacy_mix in legacy_mixnodes {
|
|
let node_id = legacy_mix.mix_id();
|
|
|
|
let routing_score = get_routing_score(
|
|
storage,
|
|
node_id,
|
|
DescribedNodeType::LegacyMixnode,
|
|
current_interval,
|
|
)
|
|
.await;
|
|
let config_score =
|
|
calculate_config_score(config_score_data, described_nodes.get_node(&node_id));
|
|
|
|
let performance = routing_score.score * config_score.score;
|
|
// map it from 0-1 range into 0-100
|
|
let scaled_performance = performance * 100.;
|
|
let legacy_performance = Uptime::new(scaled_performance as f32).into();
|
|
|
|
annotations.insert(
|
|
legacy_mix.mix_id(),
|
|
NodeAnnotation {
|
|
last_24h_performance: legacy_performance,
|
|
current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()),
|
|
detailed_performance: DetailedNodePerformance::new(
|
|
performance,
|
|
routing_score,
|
|
config_score,
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
for legacy_gateway in legacy_gateways {
|
|
let node_id = legacy_gateway.node_id;
|
|
let routing_score = get_routing_score(
|
|
storage,
|
|
node_id,
|
|
DescribedNodeType::LegacyGateway,
|
|
current_interval,
|
|
)
|
|
.await;
|
|
let config_score =
|
|
calculate_config_score(config_score_data, described_nodes.get_node(&node_id));
|
|
|
|
let performance = routing_score.score * config_score.score;
|
|
// map it from 0-1 range into 0-100
|
|
let scaled_performance = performance * 100.;
|
|
let legacy_performance = Uptime::new(scaled_performance as f32).into();
|
|
|
|
annotations.insert(
|
|
legacy_gateway.node_id,
|
|
NodeAnnotation {
|
|
last_24h_performance: legacy_performance,
|
|
current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()),
|
|
detailed_performance: DetailedNodePerformance::new(
|
|
performance,
|
|
routing_score,
|
|
config_score,
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
for nym_node in nym_nodes {
|
|
let node_id = nym_node.node_id();
|
|
let routing_score = get_routing_score(
|
|
storage,
|
|
node_id,
|
|
DescribedNodeType::NymNode,
|
|
current_interval,
|
|
)
|
|
.await;
|
|
let config_score =
|
|
calculate_config_score(config_score_data, described_nodes.get_node(&node_id));
|
|
|
|
let performance = routing_score.score * config_score.score;
|
|
// map it from 0-1 range into 0-100
|
|
let scaled_performance = performance * 100.;
|
|
let legacy_performance = Uptime::new(scaled_performance as f32).into();
|
|
|
|
annotations.insert(
|
|
nym_node.node_id(),
|
|
NodeAnnotation {
|
|
last_24h_performance: legacy_performance,
|
|
current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()),
|
|
detailed_performance: DetailedNodePerformance::new(
|
|
performance,
|
|
routing_score,
|
|
config_score,
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
annotations
|
|
}
|