// Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; use crate::support::storage::NymApiStorage; use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodePerformance}; use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::{reward_params::Performance, Interval, MixId}; use nym_mixnet_contract_common::{ GatewayBond, IdentityKey, MixNodeDetails, RewardedSetNodeStatus, RewardingParams, }; use nym_topology::NetworkAddress; use std::collections::{HashMap, HashSet}; use std::net::ToSocketAddrs; use std::str::FromStr; pub(super) fn to_rewarded_set_node_status( rewarded_set: &[MixNodeDetails], active_set: &[MixNodeDetails], ) -> HashMap { let mut rewarded_set_node_status: HashMap = rewarded_set .iter() .map(|m| (m.mix_id(), RewardedSetNodeStatus::Standby)) .collect(); for mixnode in active_set { *rewarded_set_node_status .get_mut(&mixnode.mix_id()) .expect("All active nodes are rewarded nodes") = RewardedSetNodeStatus::Active; } rewarded_set_node_status } pub(super) fn split_into_active_and_rewarded_set( mixnodes_annotated: &HashMap, rewarded_set_node_status: &HashMap, ) -> (Vec, Vec) { let rewarded_set: Vec<_> = mixnodes_annotated .values() .filter(|mixnode| rewarded_set_node_status.get(&mixnode.mix_id()).is_some()) .cloned() .collect(); let active_set: Vec<_> = rewarded_set .iter() .filter(|mixnode| { rewarded_set_node_status .get(&mixnode.mix_id()) .map_or(false, RewardedSetNodeStatus::is_active) }) .cloned() .collect(); (rewarded_set, active_set) } pub(super) async fn get_mixnode_performance_from_storage( storage: &NymApiStorage, mix_id: MixId, epoch: Interval, ) -> Option { storage .get_average_mixnode_uptime_in_the_last_24hrs( mix_id, epoch.current_epoch_end_unix_timestamp(), ) .await .ok() .map(Into::into) } pub(super) async fn get_gateway_performance_from_storage( storage: &NymApiStorage, gateway_id: &str, epoch: Interval, ) -> Option { storage .get_average_gateway_uptime_in_the_last_24hrs( gateway_id, epoch.current_epoch_end_unix_timestamp(), ) .await .ok() .map(Into::into) } pub(super) async fn annotate_nodes_with_details( storage: &NymApiStorage, mixnodes: Vec, interval_reward_params: RewardingParams, current_interval: Interval, rewarded_set: &HashMap, mix_to_family: Vec<(IdentityKey, FamilyHead)>, blacklist: &HashSet, ) -> HashMap { let mix_to_family = mix_to_family .into_iter() .collect::>(); 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 = rewarded_set.get(&mixnode.mix_id()).copied(); // 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_performance_from_storage(storage, mixnode.mix_id(), current_interval) .await .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(); // safety: this conversion is infallible let ip_addresses = match NetworkAddress::from_str(&mixnode.bond_information.mix_node.host).unwrap() { NetworkAddress::IpAddr(ip) => vec![ip], NetworkAddress::Hostname(hostname) => { // try to resolve it ( hostname.as_str(), mixnode.bond_information.mix_node.mix_port, ) .to_socket_addrs() .map(|iter| iter.map(|s| s.ip()).collect::>()) .unwrap_or_default() } }; let (estimated_operator_apy, estimated_delegators_apy) = compute_apy_from_reward(&mixnode, reward_estimate, current_interval); let family = mix_to_family .get(mixnode.bond_information.identity()) .cloned(); annotated.insert( mixnode.mix_id(), MixNodeBondAnnotated { blacklisted: blacklist.contains(&mixnode.mix_id()), mixnode_details: mixnode, stake_saturation, uncapped_stake_saturation, performance, node_performance, estimated_operator_apy, estimated_delegators_apy, family, ip_addresses, }, ); } annotated } pub(crate) async fn annotate_gateways_with_details( storage: &NymApiStorage, gateway_bonds: Vec, current_interval: Interval, blacklist: &HashSet, ) -> HashMap { let mut annotated = HashMap::new(); for gateway_bond in gateway_bonds { let performance = get_gateway_performance_from_storage( storage, gateway_bond.identity(), current_interval, ) .await .unwrap_or_default(); let node_performance = storage .construct_gateway_report(gateway_bond.identity()) .await .map(NodePerformance::from) .ok() .unwrap_or_default(); // safety: this conversion is infallible let ip_addresses = match NetworkAddress::from_str(&gateway_bond.gateway.host).unwrap() { NetworkAddress::IpAddr(ip) => vec![ip], NetworkAddress::Hostname(hostname) => { // try to resolve it (hostname.as_str(), gateway_bond.gateway.mix_port) .to_socket_addrs() .map(|iter| iter.map(|s| s.ip()).collect::>()) .unwrap_or_default() } }; annotated.insert( gateway_bond.identity().to_string(), GatewayBondAnnotated { blacklisted: blacklist.contains(&gateway_bond.gateway.identity_key), gateway_bond, self_described: None, performance, node_performance, ip_addresses, }, ); } annotated }