Skip to content

feat: key rotation #5777

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 37 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d6b17af
wip
jstuczyn Apr 11, 2025
29fae3e
wip: wrap node's sphinx key with a manager
jstuczyn Apr 11, 2025
2659284
wip: choosing correct key for packet processing
jstuczyn Apr 15, 2025
6a5de61
further propagation of key rotation information
jstuczyn Apr 24, 2025
20cf383
attaching key rotation information to reply surbs
jstuczyn Apr 25, 2025
1a4885c
added basic key rotation information to mixnet contract
jstuczyn May 1, 2025
97031a4
wip: introducing cached queries for key rotation info from nym api
jstuczyn May 1, 2025
ed82689
unified nym-api contract cache refreshing
jstuczyn May 2, 2025
c2c4efb
finish packet decoding
jstuczyn May 2, 2025
951c28c
multi api client + retrieving rotation id
jstuczyn May 2, 2025
5606b80
rotating sphinx key files
jstuczyn May 6, 2025
fa8fd42
logic for migrating config file
jstuczyn May 6, 2025
8cca596
wip: putting new sphinx keys to self described endpoints
jstuczyn May 7, 2025
0998adf
processing loop of KeyRotationController
jstuczyn May 9, 2025
483fce4
fixed sphinx key loading
jstuczyn May 9, 2025
39fc2e2
rotating bloomfilters
jstuczyn May 9, 2025
588c9d1
wired up KeyRotationController
jstuczyn May 12, 2025
0a33aa9
flushing bloomfilters to disk and loading
jstuczyn May 12, 2025
c109699
most of nym-node changes
jstuczyn May 13, 2025
e1a150d
post rebase fixes
jstuczyn May 13, 2025
0423d9c
fixes due to backwards compatible hostkeys
jstuczyn May 14, 2025
1193d0f
split http state.rs file
jstuczyn May 14, 2025
f18eb69
dont use deprecated fields
jstuczyn May 14, 2025
b2f65e5
fixed backwards compatible deserialisation of host information
jstuczyn May 14, 2025
6fa11a2
split up node describe cache
jstuczyn May 14, 2025
75eb9b8
added a dedicated CacheRefresher listener to perform full refresh out…
jstuczyn May 14, 2025
250c27a
controlling announced sphinx keys within nym-api
jstuczyn May 14, 2025
f5c3c99
retrieving rotation id when pulling topology
jstuczyn May 14, 2025
4f1d493
split nym-nodes http handlers
jstuczyn May 15, 2025
552f4a6
v2 nym-api endpoints to retrieve nodes with additional metadata infor…
jstuczyn May 16, 2025
5077132
bug fixes...
jstuczyn May 16, 2025
046a93e
additional bugfixes and guards against stuck epoch
jstuczyn May 19, 2025
65bfc43
testnet manager: set first nym-api as the rewarder
jstuczyn May 19, 2025
5db318f
fixed host information deserialisation
jstuczyn May 19, 2025
d6c5e2e
fixed panic during first key rotation
jstuczyn May 19, 2025
f5b295c
post rebase fixes
jstuczyn May 19, 2025
d9abe17
clippy
jstuczyn May 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,6 @@ utoipauto = "0.2"
uuid = "*"
vergen = { version = "=8.3.1", default-features = false }
walkdir = "2"
wasm-bindgen-test = "0.3.49"
x25519-dalek = "2.0.0"
zeroize = "1.7.0"

Expand Down Expand Up @@ -392,6 +391,7 @@ serde-wasm-bindgen = "0.6.5"
tsify = "0.4.5"
wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49"
wasm-bindgen-test = "0.3.49"
wasmtimer = "0.4.1"
web-sys = "0.3.76"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit};
use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey};
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier};
use nym_sphinx::message::NymMessage;
use nym_sphinx::params::{PacketSize, PacketType};
Expand Down Expand Up @@ -44,7 +44,10 @@ pub enum PreparationError {
}

impl PreparationError {
fn return_surbs(self, returned_surbs: Vec<ReplySurb>) -> SurbWrappedPreparationError {
fn return_surbs(
self,
returned_surbs: Vec<ReplySurbWithKeyRotation>,
) -> SurbWrappedPreparationError {
SurbWrappedPreparationError {
source: self,
returned_surbs: Some(returned_surbs),
Expand All @@ -58,7 +61,7 @@ pub struct SurbWrappedPreparationError {
#[source]
source: PreparationError,

returned_surbs: Option<Vec<ReplySurb>>,
returned_surbs: Option<Vec<ReplySurbWithKeyRotation>>,
}

impl<T> From<T> for SurbWrappedPreparationError
Expand Down Expand Up @@ -268,10 +271,10 @@ where
}
}

async fn generate_reply_surbs_with_keys(
async fn generate_reply_surbs(
&mut self,
amount: usize,
) -> Result<(Vec<ReplySurb>, Vec<SurbEncryptionKey>), PreparationError> {
) -> Result<Vec<ReplySurbWithKeyRotation>, PreparationError> {
let topology_permit = self.topology_access.get_read_permit().await;
let topology = self.get_topology(&topology_permit)?;

Expand All @@ -281,19 +284,14 @@ where
topology,
)?;

let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();

Ok((reply_surbs, reply_keys))
Ok(reply_surbs)
}

pub(crate) async fn try_send_single_surb_message(
&mut self,
target: AnonymousSenderTag,
message: ReplyMessage,
reply_surb: ReplySurb,
reply_surb: ReplySurbWithKeyRotation,
is_extra_surb_request: bool,
) -> Result<(), SurbWrappedPreparationError> {
let msg = NymMessage::new_reply(message);
Expand Down Expand Up @@ -347,7 +345,7 @@ where
pub(crate) async fn try_request_additional_reply_surbs(
&mut self,
from: AnonymousSenderTag,
reply_surb: ReplySurb,
reply_surb: ReplySurbWithKeyRotation,
amount: u32,
) -> Result<(), SurbWrappedPreparationError> {
debug!("requesting {amount} reply SURBs from {from}");
Expand Down Expand Up @@ -387,7 +385,7 @@ where
&mut self,
target: AnonymousSenderTag,
fragments: Vec<FragmentWithMaxRetransmissions>,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
lane: TransmissionLane,
) -> Result<(), SurbWrappedPreparationError> {
// TODO: technically this is performing an unnecessary cloning, but in the grand scheme of things
Expand All @@ -404,7 +402,7 @@ where
&mut self,
target: AnonymousSenderTag,
fragments: Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
) -> Result<(), SurbWrappedPreparationError> {
let prepared_fragments = self
.prepare_reply_chunks_for_sending(
Expand Down Expand Up @@ -541,8 +539,12 @@ where
) -> Result<(), PreparationError> {
debug!("Sending additional reply SURBs with packet type {packet_type}");
let sender_tag = self.get_or_create_sender_tag(&recipient);
let (reply_surbs, reply_keys) =
self.generate_reply_surbs_with_keys(amount as usize).await?;
let reply_surbs = self.generate_reply_surbs(amount as usize).await?;

let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();

let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs(
self.config.use_legacy_sphinx_format,
Expand Down Expand Up @@ -579,9 +581,12 @@ where
) -> Result<(), SurbWrappedPreparationError> {
debug!("Sending message with reply SURBs with packet type {packet_type}");
let sender_tag = self.get_or_create_sender_tag(&recipient);
let (reply_surbs, reply_keys) = self
.generate_reply_surbs_with_keys(num_reply_surbs as usize)
.await?;
let reply_surbs = self.generate_reply_surbs(num_reply_surbs as usize).await?;

let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();

let message = NymMessage::new_repliable(RepliableMessage::new_data(
self.config.use_legacy_sphinx_format,
Expand Down Expand Up @@ -629,7 +634,7 @@ where
pub(crate) async fn prepare_reply_chunks_for_sending(
&mut self,
fragments: Vec<Fragment>,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
) -> Result<Vec<PreparedFragment>, SurbWrappedPreparationError> {
debug_assert_eq!(
fragments.len(),
Expand Down Expand Up @@ -665,7 +670,7 @@ where

pub(crate) async fn try_prepare_single_reply_chunk_for_sending(
&mut self,
reply_surb: ReplySurb,
reply_surb: ReplySurbWithKeyRotation,
chunk: Fragment,
) -> Result<PreparedFragment, SurbWrappedPreparationError> {
let topology_permit = self.topology_access.get_read_permit().await;
Expand Down
4 changes: 2 additions & 2 deletions common/client-core/src/client/replies/reply_controller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use futures::StreamExt;
use log::{debug, error, info, trace, warn};
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurb;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_task::connections::{ConnectionId, TransmissionLane};
use nym_task::TaskClient;
Expand Down Expand Up @@ -499,7 +499,7 @@ where
async fn handle_received_surbs(
&mut self,
from: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
from_surb_request: bool,
) {
trace!("handling received surbs");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use futures::channel::{mpsc, oneshot};
use log::error;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurb;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_task::connections::{ConnectionId, TransmissionLane};
use std::sync::Weak;

Expand Down Expand Up @@ -81,7 +81,7 @@ impl ReplyControllerSender {
pub(crate) fn send_additional_surbs(
&self,
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
from_surb_request: bool,
) -> Result<(), ReplyControllerSenderError> {
self.0
Expand Down Expand Up @@ -167,7 +167,7 @@ pub enum ReplyControllerMessage {

AdditionalSurbs {
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
from_surb_request: bool,
},

Expand Down
71 changes: 50 additions & 21 deletions common/client-core/src/client/topology_control/nym_api_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
use async_trait::async_trait;
use log::{debug, error, warn};
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::NymTopology;
use nym_topology::{NymTopology, NymTopologyMetadata};
use nym_validator_client::UserAgent;
use rand::prelude::SliceRandom;
use rand::thread_rng;
Expand Down Expand Up @@ -89,55 +89,84 @@ impl NymApiTopologyProvider {
let rewarded_set_fut = self.validator_client.get_current_rewarded_set();

let topology = if self.config.use_extended_topology {
let all_nodes_fut = self.validator_client.get_all_basic_nodes();
let all_nodes_fut = self.validator_client.get_all_basic_nodes_with_metadata();

// Join rewarded_set_fut and all_nodes_fut concurrently
let (rewarded_set, all_nodes) = futures::try_join!(rewarded_set_fut, all_nodes_fut)
let (rewarded_set, all_nodes_res) = futures::try_join!(rewarded_set_fut, all_nodes_fut)
.inspect_err(|err| error!("failed to get network nodes: {err}"))
.ok()?;

let metadata = all_nodes_res.metadata;
let all_nodes = all_nodes_res.nodes;

debug!(
"there are {} nodes on the network (before filtering)",
all_nodes.len()
);
let mut topology = NymTopology::new_empty(rewarded_set);
topology.add_additional_nodes(all_nodes.iter().filter(|n| {
n.performance.round_to_integer() >= self.config.min_node_performance()
}));

topology
let nodes_filtered = all_nodes
.into_iter()
.filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance())
.collect::<Vec<_>>();

NymTopology::new(
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
rewarded_set,
Vec::new(),
)
.with_skimmed_nodes(&nodes_filtered)
} else {
// if we're not using extended topology, we're only getting active set mixnodes and gateways

let mixnodes_fut = self
.validator_client
.get_all_basic_active_mixing_assigned_nodes();
.get_all_basic_active_mixing_assigned_nodes_with_metadata();

// TODO: we really should be getting ACTIVE gateways only
let gateways_fut = self.validator_client.get_all_basic_entry_assigned_nodes();
let gateways_fut = self
.validator_client
.get_all_basic_entry_assigned_nodes_v2();

let (rewarded_set, mixnodes, gateways) =
let (rewarded_set, mixnodes_res, gateways_res) =
futures::try_join!(rewarded_set_fut, mixnodes_fut, gateways_fut)
.inspect_err(|err| {
error!("failed to get network nodes: {err}");
})
.ok()?;

let metadata = mixnodes_res.metadata;
let mixnodes = mixnodes_res.nodes;

if gateways_res.metadata != metadata {
warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
return None;
}

let gateways = gateways_res.nodes;

debug!(
"there are {} mixnodes and {} gateways in total (before performance filtering)",
mixnodes.len(),
gateways.len()
);

let mut topology = NymTopology::new_empty(rewarded_set);
topology.add_additional_nodes(mixnodes.iter().filter(|m| {
m.performance.round_to_integer() >= self.config.min_mixnode_performance
}));
topology.add_additional_nodes(gateways.iter().filter(|m| {
m.performance.round_to_integer() >= self.config.min_gateway_performance
}));

topology
let mut nodes = Vec::new();
for mix in mixnodes {
if mix.performance.round_to_integer() >= self.config.min_mixnode_performance {
nodes.push(mix)
}
}
for gateway in gateways {
if gateway.performance.round_to_integer() >= self.config.min_gateway_performance {
nodes.push(gateway)
}
}

NymTopology::new(
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
rewarded_set,
Vec::new(),
)
.with_skimmed_nodes(&nodes)
};

if !topology.is_minimally_routable() {
Expand Down
2 changes: 1 addition & 1 deletion common/client-core/src/init/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ pub async fn gateways_for_init<R: Rng>(

log::debug!("Fetching list of gateways from: {nym_api}");

let gateways = client.get_all_basic_entry_assigned_nodes().await?;
let gateways = client.get_all_basic_entry_assigned_nodes_v2().await?.nodes;
info!("nym api reports {} gateways", gateways.len());

log::trace!("Gateways: {:#?}", gateways);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* Copyright 2025 - Nym Technologies SA <[email protected]>
* SPDX-License-Identifier: Apache-2.0
*/

-- default value of 0 implies 'unknown' variant
ALTER TABLE reply_surb
ADD COLUMN encoded_key_rotation TINYINT NOT NULL DEFAULT 0;
Loading
Loading