feat: key rotation (#5777)

* 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
This commit is contained in:
Jędrzej Stuczyński
2025-06-03 12:22:51 +02:00
committed by GitHub
parent adbe0392ca
commit d8c84cc4d6
204 changed files with 9392 additions and 3819 deletions
+18 -70
View File
@@ -5,14 +5,9 @@
use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer;
use nym_api_requests::models::InclusionProbability;
use nym_contracts_common::truncate_decimal;
use nym_mixnet_contract_common::{NodeId, RewardingParams};
use nym_mixnet_contract_common::NodeId;
use serde::Serialize;
use std::time::Duration;
use tracing::error;
const MAX_SIMULATION_SAMPLES: u64 = 5000;
const MAX_SIMULATION_TIME_SEC: u64 = 15;
#[deprecated]
#[derive(Clone, Default, Serialize, schemars::JsonSchema)]
@@ -25,11 +20,24 @@ pub(crate) struct InclusionProbabilities {
}
impl InclusionProbabilities {
pub(crate) fn compute(
pub(crate) fn legacy_zero(
mixnodes: &[LegacyMixNodeDetailsWithLayer],
params: RewardingParams,
) -> Option<InclusionProbabilities> {
compute_inclusion_probabilities(mixnodes, params)
) -> InclusionProbabilities {
// (all legacy mixnodes have 0% chance of being selected)
InclusionProbabilities {
inclusion_probabilities: mixnodes
.iter()
.map(|m| InclusionProbability {
mix_id: m.mix_id(),
in_active: 0.0,
in_reserve: 0.0,
})
.collect(),
samples: 0,
elapsed: Default::default(),
delta_max: 0.0,
delta_l2: 0.0,
}
}
pub(crate) fn node(&self, mix_id: NodeId) -> Option<&InclusionProbability> {
@@ -38,63 +46,3 @@ impl InclusionProbabilities {
.find(|x| x.mix_id == mix_id)
}
}
#[deprecated]
fn compute_inclusion_probabilities(
mixnodes: &[LegacyMixNodeDetailsWithLayer],
params: RewardingParams,
) -> Option<InclusionProbabilities> {
let active_set_size = params.active_set_size();
let standby_set_size = params.rewarded_set.standby;
// Unzip list of total bonds into ids and bonds.
// We need to go through this zip/unzip procedure to make sure we have matching identities
// for the input to the simulator, which assumes the identity is the position in the vec
let (ids, mixnode_total_bonds) = unzip_into_mixnode_ids_and_total_bonds(mixnodes);
// Compute inclusion probabilitites and keep track of how long time it took.
let mut rng = rand::thread_rng();
let results = nym_inclusion_probability::simulate_selection_probability_mixnodes(
&mixnode_total_bonds,
active_set_size as usize,
standby_set_size as usize,
MAX_SIMULATION_SAMPLES,
Duration::from_secs(MAX_SIMULATION_TIME_SEC),
&mut rng,
)
.inspect_err(|err| error!("{err}"))
.ok()?;
Some(InclusionProbabilities {
inclusion_probabilities: zip_ids_together_with_results(&ids, &results),
samples: results.samples,
elapsed: results.time,
delta_max: results.delta_max,
delta_l2: results.delta_l2,
})
}
fn unzip_into_mixnode_ids_and_total_bonds(
mixnodes: &[LegacyMixNodeDetailsWithLayer],
) -> (Vec<NodeId>, Vec<u128>) {
mixnodes
.iter()
.map(|m| (m.mix_id(), truncate_decimal(m.total_stake()).u128()))
.unzip()
}
#[deprecated]
fn zip_ids_together_with_results(
ids: &[NodeId],
results: &nym_inclusion_probability::SelectionProbability,
) -> Vec<InclusionProbability> {
ids.iter()
.zip(results.active_set_probability.iter())
.zip(results.reserve_set_probability.iter())
.map(|((&mix_id, a), r)| InclusionProbability {
mix_id,
in_active: *a,
in_reserve: *r,
})
.collect()
}
+7 -3
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use self::data::NodeStatusCacheData;
use crate::support::caching::cache::UninitialisedCache;
use crate::support::caching::Cache;
use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation};
use nym_contracts_common::IdentityKey;
@@ -22,9 +23,6 @@ pub mod refresher;
#[derive(Debug, Error)]
enum NodeStatusCacheError {
#[error("failed to simulate selection probabilities for mixnodes, not updating cache")]
SimulationFailed,
#[error("the current interval information is not available at the moment")]
SourceDataMissing,
@@ -32,6 +30,12 @@ enum NodeStatusCacheError {
UnavailableDescribedCache,
}
impl From<UninitialisedCache> for NodeStatusCacheError {
fn from(_: UninitialisedCache) -> Self {
NodeStatusCacheError::SourceDataMissing
}
}
/// A node status cache suitable for caching values computed in one sweep, such as active set
/// inclusion probabilities that are computed for all mixnodes at the same time.
///
+27 -12
View File
@@ -1,7 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_describe_cache::DescribedNodes;
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};
@@ -9,7 +9,6 @@ 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::DescribedNodeType::{LegacyGateway, LegacyMixnode, NymNode};
use nym_api_requests::models::{
ConfigScore, DescribedNodeType, DetailedNodePerformance, GatewayBondAnnotated,
MixNodeBondAnnotated, NodeAnnotation, NodePerformance, NymNodeDescription, RoutingScore,
@@ -18,7 +17,7 @@ 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, HashSet};
use std::collections::HashMap;
use tracing::trace;
pub(super) async fn get_mixnode_reliability_from_storage(
@@ -166,7 +165,6 @@ pub(super) async fn annotate_legacy_mixnodes_nodes_with_details(
interval_reward_params: RewardingParams,
current_interval: Interval,
rewarded_set: &CachedEpochRewardedSet,
blacklist: &HashSet<NodeId>,
) -> HashMap<NodeId, MixNodeBondAnnotated> {
let mut annotated = HashMap::new();
for mixnode in mixnodes {
@@ -216,7 +214,8 @@ pub(super) async fn annotate_legacy_mixnodes_nodes_with_details(
annotated.insert(
mixnode.mix_id(),
MixNodeBondAnnotated {
blacklisted: blacklist.contains(&mixnode.mix_id()),
// all legacy nodes are always blacklisted
blacklisted: true,
mixnode_details: mixnode,
stake_saturation,
uncapped_stake_saturation,
@@ -236,7 +235,6 @@ pub(crate) async fn annotate_legacy_gateways_with_details(
storage: &NymApiStorage,
gateway_bonds: Vec<LegacyGatewayBondWithId>,
current_interval: Interval,
blacklist: &HashSet<NodeId>,
) -> HashMap<NodeId, GatewayBondAnnotated> {
let mut annotated = HashMap::new();
for gateway_bond in gateway_bonds {
@@ -263,7 +261,8 @@ pub(crate) async fn annotate_legacy_gateways_with_details(
annotated.insert(
gateway_bond.node_id,
GatewayBondAnnotated {
blacklisted: blacklist.contains(&gateway_bond.node_id),
// all legacy nodes are always blacklisted
blacklisted: true,
gateway_bond,
self_described: None,
performance,
@@ -291,8 +290,13 @@ pub(crate) async fn produce_node_annotations(
for legacy_mix in legacy_mixnodes {
let node_id = legacy_mix.mix_id();
let routing_score =
get_routing_score(storage, node_id, LegacyMixnode, current_interval).await;
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));
@@ -317,8 +321,13 @@ pub(crate) async fn produce_node_annotations(
for legacy_gateway in legacy_gateways {
let node_id = legacy_gateway.node_id;
let routing_score =
get_routing_score(storage, node_id, LegacyGateway, current_interval).await;
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));
@@ -343,7 +352,13 @@ pub(crate) async fn produce_node_annotations(
for nym_node in nym_nodes {
let node_id = nym_node.node_id();
let routing_score = get_routing_score(storage, node_id, NymNode, current_interval).await;
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));
+10 -33
View File
@@ -2,14 +2,12 @@
// SPDX-License-Identifier: GPL-3.0-only
use super::NodeStatusCache;
use crate::node_describe_cache::DescribedNodes;
use crate::node_describe_cache::cache::DescribedNodes;
use crate::node_status_api::cache::node_sets::produce_node_annotations;
use crate::support::caching::cache::SharedCache;
use crate::{
node_status_api::cache::{inclusion_probabilities, NodeStatusCacheError},
nym_contract_cache::cache::NymContractCache,
storage::NymApiStorage,
support::caching::CacheNotification,
node_status_api::cache::NodeStatusCacheError, nym_contract_cache::cache::NymContractCache,
storage::NymApiStorage, support::caching::CacheNotification,
};
use ::time::OffsetDateTime;
use nym_task::TaskClient;
@@ -17,7 +15,7 @@ use std::collections::HashMap;
use std::time::Duration;
use tokio::sync::watch;
use tokio::time;
use tracing::{error, info, trace, warn};
use tracing::{info, trace, warn};
// Long running task responsible for keeping the node status cache up-to-date.
pub struct NodeStatusCacheRefresher {
@@ -139,35 +137,16 @@ impl NodeStatusCacheRefresher {
// Fetch contract cache data to work with
let mixnode_details = self.contract_cache.legacy_mixnodes_all().await;
let interval_reward_params = self.contract_cache.interval_reward_params().await;
let current_interval = self.contract_cache.current_interval().await;
let rewarded_set = self.contract_cache.rewarded_set_owned().await;
let interval_reward_params = self.contract_cache.interval_reward_params().await?;
let current_interval = self.contract_cache.current_interval().await?;
let rewarded_set = self.contract_cache.rewarded_set_owned().await?;
let gateway_bonds = self.contract_cache.legacy_gateways_all().await;
let nym_nodes = self.contract_cache.nym_nodes().await;
let config_score_data = self
.contract_cache
.config_score_data_owned()
.await
.into_inner()
.ok_or(NodeStatusCacheError::SourceDataMissing)?;
// get blacklists
let mixnodes_blacklist = self.contract_cache.mixnodes_blacklist().await;
let gateways_blacklist = self.contract_cache.gateways_blacklist().await;
let interval_reward_params =
interval_reward_params.ok_or(NodeStatusCacheError::SourceDataMissing)?;
let current_interval = current_interval.ok_or(NodeStatusCacheError::SourceDataMissing)?;
let config_score_data = self.contract_cache.maybe_config_score_data().await?;
// Compute inclusion probabilities
let inclusion_probabilities = inclusion_probabilities::InclusionProbabilities::compute(
&mixnode_details,
interval_reward_params,
)
.ok_or_else(|| {
error!("Failed to simulate selection probabilities for mixnodes, not updating cache");
NodeStatusCacheError::SimulationFailed
})?;
// (all legacy mixnodes have 0% chance of being selected)
let inclusion_probabilities = crate::node_status_api::cache::inclusion_probabilities::InclusionProbabilities::legacy_zero(&mixnode_details);
let Ok(described) = self.described_cache.get().await else {
return Err(NodeStatusCacheError::UnavailableDescribedCache);
@@ -198,7 +177,6 @@ impl NodeStatusCacheRefresher {
interval_reward_params,
current_interval,
&rewarded_set,
&mixnodes_blacklist,
)
.await;
@@ -207,7 +185,6 @@ impl NodeStatusCacheRefresher {
&self.storage,
gateway_bonds,
current_interval,
&gateways_blacklist,
)
.await;