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
+63 -19
View File
@@ -2,18 +2,26 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::NymNodeError;
use crate::node::key_rotation::active_keys::ActiveSphinxKeys;
use crate::node::routing_filter::network_filter::NetworkRoutingFilter;
use async_trait::async_trait;
use nym_crypto::asymmetric::ed25519;
use nym_gateway::node::UserAgent;
use nym_node_metrics::prometheus_wrapper::{PrometheusMetric, PROMETHEUS_METRICS};
use nym_task::ShutdownToken;
use nym_topology::node::RoutingNode;
use nym_topology::{EpochRewardedSet, NymTopology, Role, TopologyProvider};
use nym_topology::{
EntryDetails, EpochRewardedSet, NodeId, NymTopology, NymTopologyMetadata, Role,
TopologyProvider,
};
use nym_validator_client::nym_api::NymApiClientExt;
use nym_validator_client::nym_nodes::{NodesByAddressesResponse, SkimmedNode};
use nym_validator_client::nym_nodes::{
NodesByAddressesResponse, SkimmedNode, SkimmedNodesWithMetadata,
};
use nym_validator_client::{NymApiClient, ValidatorClientError};
use std::collections::HashSet;
use std::net::IpAddr;
use std::net::{IpAddr, SocketAddr};
use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
@@ -22,6 +30,8 @@ use tracing::log::error;
use tracing::{debug, trace, warn};
use url::Url;
const LOCAL_NODE_ID: NodeId = 1234567890;
struct NodesQuerier {
client: NymApiClient,
nym_api_urls: Vec<Url>,
@@ -53,10 +63,10 @@ impl NodesQuerier {
res
}
async fn current_nymnodes(&mut self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
async fn current_nymnodes(&mut self) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
let res = self
.client
.get_all_basic_nodes()
.get_all_basic_nodes_with_metadata()
.await
.inspect_err(|err| error!("failed to get network nodes: {err}"));
@@ -84,16 +94,40 @@ impl NodesQuerier {
}
}
pub(crate) struct LocalGatewayNode {
pub(crate) active_sphinx_keys: ActiveSphinxKeys,
pub(crate) mix_host: SocketAddr,
pub(crate) identity_key: ed25519::PublicKey,
pub(crate) entry: EntryDetails,
}
impl LocalGatewayNode {
pub(crate) fn to_routing_node(&self) -> RoutingNode {
RoutingNode {
node_id: LOCAL_NODE_ID,
mix_host: self.mix_host,
entry: Some(self.entry.clone()),
identity_key: self.identity_key,
sphinx_key: self.active_sphinx_keys.primary().deref().x25519_pubkey(),
supported_roles: nym_topology::SupportedRoles {
mixnode: false,
mixnet_entry: true,
mixnet_exit: true,
},
}
}
}
#[derive(Clone)]
pub struct CachedTopologyProvider {
gateway_node: Arc<RoutingNode>,
gateway_node: Arc<LocalGatewayNode>,
cached_network: CachedNetwork,
min_mix_performance: u8,
}
impl CachedTopologyProvider {
pub(crate) fn new(
gateway_node: RoutingNode,
gateway_node: LocalGatewayNode,
cached_network: CachedNetwork,
min_mix_performance: u8,
) -> Self {
@@ -111,20 +145,24 @@ impl TopologyProvider for CachedTopologyProvider {
let network_guard = self.cached_network.inner.read().await;
let self_node = self.gateway_node.identity_key;
let mut topology = NymTopology::new_empty(network_guard.rewarded_set.clone())
.with_additional_nodes(network_guard.network_nodes.iter().filter(|node| {
if node.supported_roles.mixnode {
node.performance.round_to_integer() >= self.min_mix_performance
} else {
true
}
}));
let mut topology = NymTopology::new(
network_guard.topology_metadata,
network_guard.rewarded_set.clone(),
Vec::new(),
)
.with_additional_nodes(network_guard.network_nodes.iter().filter(|node| {
if node.supported_roles.mixnode {
node.performance.round_to_integer() >= self.min_mix_performance
} else {
true
}
}));
if !topology.has_node_details(self.gateway_node.node_id) {
if !topology.has_node(self.gateway_node.identity_key) {
debug!("{self_node} didn't exist in topology. inserting it.",);
topology.insert_node_details(self.gateway_node.as_ref().clone());
topology.insert_node_details(self.gateway_node.to_routing_node());
}
topology.force_set_active(self.gateway_node.node_id, Role::EntryGateway);
topology.force_set_active(LOCAL_NODE_ID, Role::EntryGateway);
Some(topology)
}
@@ -140,6 +178,7 @@ impl CachedNetwork {
CachedNetwork {
inner: Arc::new(RwLock::new(CachedNetworkInner {
rewarded_set: Default::default(),
topology_metadata: Default::default(),
network_nodes: vec![],
})),
}
@@ -148,6 +187,7 @@ impl CachedNetwork {
struct CachedNetworkInner {
rewarded_set: EpochRewardedSet,
topology_metadata: NymTopologyMetadata,
network_nodes: Vec<SkimmedNode>,
}
@@ -235,7 +275,9 @@ impl NetworkRefresher {
async fn refresh_network_nodes_inner(&mut self) -> Result<(), ValidatorClientError> {
let rewarded_set = self.querier.rewarded_set().await?;
let nodes = self.querier.current_nymnodes().await?;
let res = self.querier.current_nymnodes().await?;
let nodes = res.nodes;
let metadata = res.metadata;
// collect all known/allowed nodes information
let known_nodes = nodes
@@ -264,6 +306,8 @@ impl NetworkRefresher {
self.routing_filter.pending.clear().await;
let mut network_guard = self.network.inner.write().await;
network_guard.topology_metadata =
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id);
network_guard.network_nodes = nodes;
network_guard.rewarded_set = rewarded_set;