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:
committed by
GitHub
parent
adbe0392ca
commit
d8c84cc4d6
Generated
+4
-2
@@ -4912,6 +4912,7 @@ dependencies = [
|
||||
"tendermint-rpc",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tracing",
|
||||
"ts-rs",
|
||||
"utoipa",
|
||||
]
|
||||
@@ -6246,6 +6247,7 @@ dependencies = [
|
||||
"nym-wireguard",
|
||||
"nym-wireguard-types",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
@@ -6484,6 +6486,7 @@ version = "0.3.0"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6756,7 +6759,6 @@ dependencies = [
|
||||
"nym-topology",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
@@ -6800,8 +6802,8 @@ dependencies = [
|
||||
name = "nym-sphinx-forwarding"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nym-outfox",
|
||||
"nym-sphinx-addressing",
|
||||
"nym-sphinx-anonymous-replies",
|
||||
"nym-sphinx-params",
|
||||
"nym-sphinx-types",
|
||||
"thiserror 2.0.12",
|
||||
|
||||
+1
-1
@@ -349,7 +349,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"
|
||||
|
||||
@@ -397,6 +396,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"
|
||||
|
||||
|
||||
@@ -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};
|
||||
@@ -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),
|
||||
@@ -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
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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);
|
||||
@@ -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}");
|
||||
@@ -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
|
||||
@@ -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(
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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(),
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -167,7 +167,7 @@ pub enum ReplyControllerMessage {
|
||||
|
||||
AdditionalSurbs {
|
||||
sender_tag: AnonymousSenderTag,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
from_surb_request: bool,
|
||||
},
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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()
|
||||
}));
|
||||
let nodes_filtered = all_nodes
|
||||
.into_iter()
|
||||
.filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
topology
|
||||
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
|
||||
}));
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
topology
|
||||
NymTopology::new(
|
||||
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
|
||||
rewarded_set,
|
||||
Vec::new(),
|
||||
)
|
||||
.with_skimmed_nodes(&nodes)
|
||||
};
|
||||
|
||||
if !topology.is_minimally_routable() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
* 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;
|
||||
@@ -205,7 +205,10 @@ impl StorageManager {
|
||||
) -> Result<Vec<StoredReplySurb>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
StoredReplySurb,
|
||||
"SELECT * FROM reply_surb WHERE reply_surb_sender_id = ?",
|
||||
r#"
|
||||
SELECT reply_surb_sender_id, reply_surb, encoded_key_rotation as "encoded_key_rotation: u8" FROM reply_surb
|
||||
WHERE reply_surb_sender_id = ?
|
||||
"#,
|
||||
sender_id
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
@@ -230,10 +233,11 @@ impl StorageManager {
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO reply_surb(reply_surb_sender_id, reply_surb) VALUES (?, ?);
|
||||
INSERT INTO reply_surb(reply_surb_sender_id, reply_surb, encoded_key_rotation) VALUES (?, ?, ?);
|
||||
"#,
|
||||
stored_reply_surb.reply_surb_sender_id,
|
||||
stored_reply_surb.reply_surb
|
||||
stored_reply_surb.reply_surb,
|
||||
stored_reply_surb.encoded_key_rotation
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
@@ -8,8 +8,10 @@ use nym_crypto::Digest;
|
||||
use nym_sphinx::addressing::clients::{Recipient, RecipientBytes};
|
||||
use nym_sphinx::anonymous_replies::encryption_key::EncryptionKeyDigest;
|
||||
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE};
|
||||
use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey, SurbEncryptionKeySize};
|
||||
use nym_sphinx::params::ReplySurbKeyDigestAlgorithm;
|
||||
use nym_sphinx::anonymous_replies::{
|
||||
ReplySurb, ReplySurbWithKeyRotation, SurbEncryptionKey, SurbEncryptionKeySize,
|
||||
};
|
||||
use nym_sphinx::params::{ReplySurbKeyDigestAlgorithm, SphinxKeyRotation};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StoredSenderTag {
|
||||
@@ -146,24 +148,40 @@ impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, i64) {
|
||||
pub struct StoredReplySurb {
|
||||
pub reply_surb_sender_id: i64,
|
||||
pub reply_surb: Vec<u8>,
|
||||
|
||||
// encodes only whether it's 'even', 'odd' or 'unknown' (default)
|
||||
// and not the whole id because that's redundant
|
||||
pub encoded_key_rotation: u8,
|
||||
}
|
||||
|
||||
impl StoredReplySurb {
|
||||
pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurb) -> Self {
|
||||
pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurbWithKeyRotation) -> Self {
|
||||
StoredReplySurb {
|
||||
reply_surb_sender_id,
|
||||
reply_surb: reply_surb.to_bytes(),
|
||||
reply_surb: reply_surb.inner_reply_surb().to_bytes(),
|
||||
encoded_key_rotation: reply_surb.key_rotation() as u8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<StoredReplySurb> for ReplySurb {
|
||||
impl TryFrom<StoredReplySurb> for ReplySurbWithKeyRotation {
|
||||
type Error = StorageError;
|
||||
|
||||
fn try_from(value: StoredReplySurb) -> Result<Self, Self::Error> {
|
||||
ReplySurb::from_bytes(&value.reply_surb).map_err(|err| StorageError::CorruptedData {
|
||||
details: format!("failed to recover the reply surb: {err}"),
|
||||
})
|
||||
let key_rotation =
|
||||
SphinxKeyRotation::try_from(value.encoded_key_rotation).map_err(|err| {
|
||||
StorageError::CorruptedData {
|
||||
details: format!("stored key rotation was malformed: {err}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
let reply_surb = ReplySurb::from_bytes(&value.reply_surb).map_err(|err| {
|
||||
StorageError::CorruptedData {
|
||||
details: format!("failed to recover the reply surb: {err}"),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(reply_surb.with_key_rotation(key_rotation))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use dashmap::iter::Iter;
|
||||
use dashmap::DashMap;
|
||||
use log::trace;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_sphinx::anonymous_replies::ReplySurb;
|
||||
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -134,7 +134,7 @@ impl ReceivedReplySurbsMap {
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
amount: usize,
|
||||
) -> (Option<Vec<ReplySurb>>, usize) {
|
||||
) -> (Option<Vec<ReplySurbWithKeyRotation>>, usize) {
|
||||
if let Some(mut entry) = self.inner.data.get_mut(target) {
|
||||
let surbs_left = entry.items_left();
|
||||
if surbs_left < self.min_surb_threshold() + amount {
|
||||
@@ -150,7 +150,7 @@ impl ReceivedReplySurbsMap {
|
||||
pub fn get_reply_surb_ignoring_threshold(
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
) -> Option<(Option<ReplySurb>, usize)> {
|
||||
) -> Option<(Option<ReplySurbWithKeyRotation>, usize)> {
|
||||
self.inner
|
||||
.data
|
||||
.get_mut(target)
|
||||
@@ -160,7 +160,7 @@ impl ReceivedReplySurbsMap {
|
||||
pub fn get_reply_surb(
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
) -> Option<(Option<ReplySurb>, usize)> {
|
||||
) -> Option<(Option<ReplySurbWithKeyRotation>, usize)> {
|
||||
self.inner.data.get_mut(target).map(|mut entry| {
|
||||
let surbs_left = entry.items_left();
|
||||
if surbs_left < self.min_surb_threshold() {
|
||||
@@ -171,7 +171,7 @@ impl ReceivedReplySurbsMap {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert_surbs<I: IntoIterator<Item = ReplySurb>>(
|
||||
pub fn insert_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
surbs: I,
|
||||
@@ -189,14 +189,14 @@ impl ReceivedReplySurbsMap {
|
||||
pub struct ReceivedReplySurbs {
|
||||
// in the future we'd probably want to put extra data here to indicate when the SURBs got received
|
||||
// so we could invalidate entries from the previous key rotations
|
||||
data: VecDeque<ReplySurb>,
|
||||
data: VecDeque<ReplySurbWithKeyRotation>,
|
||||
|
||||
pending_reception: u32,
|
||||
surbs_last_received_at_timestamp: i64,
|
||||
}
|
||||
|
||||
impl ReceivedReplySurbs {
|
||||
fn new(initial_surbs: VecDeque<ReplySurb>) -> Self {
|
||||
fn new(initial_surbs: VecDeque<ReplySurbWithKeyRotation>) -> Self {
|
||||
ReceivedReplySurbs {
|
||||
data: initial_surbs,
|
||||
pending_reception: 0,
|
||||
@@ -206,7 +206,7 @@ impl ReceivedReplySurbs {
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
|
||||
pub fn new_retrieved(
|
||||
surbs: Vec<ReplySurb>,
|
||||
surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
surbs_last_received_at_timestamp: i64,
|
||||
) -> ReceivedReplySurbs {
|
||||
ReceivedReplySurbs {
|
||||
@@ -217,7 +217,7 @@ impl ReceivedReplySurbs {
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
|
||||
pub fn surbs_ref(&self) -> &VecDeque<ReplySurb> {
|
||||
pub fn surbs_ref(&self) -> &VecDeque<ReplySurbWithKeyRotation> {
|
||||
&self.data
|
||||
}
|
||||
|
||||
@@ -243,7 +243,10 @@ impl ReceivedReplySurbs {
|
||||
self.pending_reception = 0;
|
||||
}
|
||||
|
||||
pub fn get_reply_surbs(&mut self, amount: usize) -> (Option<Vec<ReplySurb>>, usize) {
|
||||
pub fn get_reply_surbs(
|
||||
&mut self,
|
||||
amount: usize,
|
||||
) -> (Option<Vec<ReplySurbWithKeyRotation>>, usize) {
|
||||
if self.items_left() < amount {
|
||||
(None, self.items_left())
|
||||
} else {
|
||||
@@ -252,11 +255,11 @@ impl ReceivedReplySurbs {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_reply_surb(&mut self) -> (Option<ReplySurb>, usize) {
|
||||
pub fn get_reply_surb(&mut self) -> (Option<ReplySurbWithKeyRotation>, usize) {
|
||||
(self.pop_surb(), self.items_left())
|
||||
}
|
||||
|
||||
fn pop_surb(&mut self) -> Option<ReplySurb> {
|
||||
fn pop_surb(&mut self) -> Option<ReplySurbWithKeyRotation> {
|
||||
self.data.pop_front()
|
||||
}
|
||||
|
||||
@@ -265,7 +268,10 @@ impl ReceivedReplySurbs {
|
||||
}
|
||||
|
||||
// realistically we're always going to be getting multiple surbs at once
|
||||
pub fn insert_reply_surbs<I: IntoIterator<Item = ReplySurb>>(&mut self, surbs: I) {
|
||||
pub fn insert_reply_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
|
||||
&mut self,
|
||||
surbs: I,
|
||||
) {
|
||||
let mut v = surbs.into_iter().collect::<VecDeque<_>>();
|
||||
trace!("storing {} surbs in the storage", v.len());
|
||||
self.data.append(&mut v);
|
||||
|
||||
@@ -21,8 +21,8 @@ use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_requests::registration::handshake::client_handshake;
|
||||
use nym_gateway_requests::{
|
||||
BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt,
|
||||
SensitiveServerResponse, ServerResponse, SharedGatewayKey, SharedSymmetricKey,
|
||||
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
|
||||
GatewayRequestsError, SensitiveServerResponse, ServerResponse, SharedGatewayKey,
|
||||
SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
|
||||
};
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_statistics_common::clients::connection::ConnectionStatsEvent;
|
||||
@@ -662,6 +662,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv();
|
||||
let supports_auth_v2 = gw_protocol.supports_authenticate_v2();
|
||||
let supports_key_rotation_info = gw_protocol.supports_key_rotation_packet();
|
||||
|
||||
if !supports_aes_gcm_siv {
|
||||
warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV");
|
||||
@@ -669,6 +670,9 @@ impl<C, St> GatewayClient<C, St> {
|
||||
if !supports_auth_v2 {
|
||||
warn!("this gateway is on an old version that doesn't support authentication v2")
|
||||
}
|
||||
if !supports_key_rotation_info {
|
||||
warn!("this gateway is on an old version that doesn't support key rotation packets")
|
||||
}
|
||||
|
||||
if self.authenticated {
|
||||
debug!("Already authenticated");
|
||||
@@ -849,6 +853,22 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
}
|
||||
|
||||
fn mix_packet_to_ws_message(&self, packet: MixPacket) -> Result<Message, GatewayRequestsError> {
|
||||
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
|
||||
// be more explicit in the naming?
|
||||
let req = if self.negotiated_protocol.supports_key_rotation_packet() {
|
||||
BinaryRequest::ForwardSphinxV2 { packet }
|
||||
} else {
|
||||
BinaryRequest::ForwardSphinx { packet }
|
||||
};
|
||||
|
||||
req.into_ws_message(
|
||||
self.shared_key
|
||||
.as_ref()
|
||||
.expect("no shared key present even though we're authenticated!"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn batch_send_mix_packets(
|
||||
&mut self,
|
||||
packets: Vec<MixPacket>,
|
||||
@@ -877,13 +897,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
let messages: Result<Vec<_>, _> = packets
|
||||
.into_iter()
|
||||
.map(|mix_packet| {
|
||||
BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
|
||||
self.shared_key
|
||||
.as_ref()
|
||||
.expect("no shared key present even though we're authenticated!"),
|
||||
)
|
||||
})
|
||||
.map(|mix_packet| self.mix_packet_to_ws_message(mix_packet))
|
||||
.collect();
|
||||
|
||||
if let Err(err) = self
|
||||
@@ -949,13 +963,8 @@ impl<C, St> GatewayClient<C, St> {
|
||||
if !self.connection.is_established() {
|
||||
return Err(GatewayClientError::ConnectionNotEstablished);
|
||||
}
|
||||
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
|
||||
// be more explicit in the naming?
|
||||
let msg = BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
|
||||
self.shared_key
|
||||
.as_ref()
|
||||
.expect("no shared key present even though we're authenticated!"),
|
||||
)?;
|
||||
|
||||
let msg = self.mix_packet_to_ws_message(mix_packet)?;
|
||||
self.send_with_reconnection_on_failure(msg).await
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
|
||||
use dashmap::DashMap;
|
||||
use futures::StreamExt;
|
||||
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
use nym_sphinx::params::PacketType;
|
||||
use nym_sphinx::NymPacket;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::ops::Deref;
|
||||
@@ -49,12 +47,7 @@ impl Config {
|
||||
pub trait SendWithoutResponse {
|
||||
// Without response in this context means we will not listen for anything we might get back (not
|
||||
// that we should get anything), including any possible io errors
|
||||
fn send_without_response(
|
||||
&self,
|
||||
address: NymNodeRoutingAddress,
|
||||
packet: NymPacket,
|
||||
packet_type: PacketType,
|
||||
) -> io::Result<()>;
|
||||
fn send_without_response(&self, packet: MixPacket) -> io::Result<()>;
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
@@ -65,7 +58,7 @@ pub struct Client {
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ActiveConnections {
|
||||
inner: Arc<DashMap<NymNodeRoutingAddress, ConnectionSender>>,
|
||||
inner: Arc<DashMap<SocketAddr, ConnectionSender>>,
|
||||
}
|
||||
|
||||
impl ActiveConnections {
|
||||
@@ -82,7 +75,7 @@ impl ActiveConnections {
|
||||
}
|
||||
|
||||
impl Deref for ActiveConnections {
|
||||
type Target = DashMap<NymNodeRoutingAddress, ConnectionSender>;
|
||||
type Target = DashMap<SocketAddr, ConnectionSender>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
@@ -196,7 +189,7 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_connection(&self, address: NymNodeRoutingAddress, pending_packet: FramedNymPacket) {
|
||||
fn make_connection(&self, address: SocketAddr, pending_packet: FramedNymPacket) {
|
||||
let (sender, receiver) = mpsc::channel(self.config.maximum_connection_buffer_size);
|
||||
|
||||
// this CAN'T fail because we just created the channel which has a non-zero capacity
|
||||
@@ -233,7 +226,7 @@ impl Client {
|
||||
|
||||
connections_count.fetch_add(1, Ordering::SeqCst);
|
||||
ManagedConnection::new(
|
||||
address.into(),
|
||||
address,
|
||||
receiver,
|
||||
initial_connection_timeout,
|
||||
current_reconnection_attempt,
|
||||
@@ -246,18 +239,14 @@ impl Client {
|
||||
}
|
||||
|
||||
impl SendWithoutResponse for Client {
|
||||
fn send_without_response(
|
||||
&self,
|
||||
address: NymNodeRoutingAddress,
|
||||
packet: NymPacket,
|
||||
packet_type: PacketType,
|
||||
) -> io::Result<()> {
|
||||
trace!("Sending packet to {address:?}");
|
||||
let framed_packet = FramedNymPacket::new(packet, packet_type);
|
||||
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> {
|
||||
let address = packet.next_hop_address();
|
||||
trace!("Sending packet to {address}");
|
||||
let framed_packet = FramedNymPacket::from(packet);
|
||||
|
||||
let Some(sender) = self.active_connections.get_mut(&address) else {
|
||||
// there was never a connection to begin with
|
||||
debug!("establishing initial connection to {}", address);
|
||||
debug!("establishing initial connection to {address}");
|
||||
// it's not a 'big' error, but we did not manage to send the packet, but queue the packet
|
||||
// for sending for as soon as the connection is created
|
||||
self.make_connection(address, framed_packet);
|
||||
|
||||
@@ -25,7 +25,9 @@ use nym_api_requests::models::{
|
||||
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
|
||||
};
|
||||
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
|
||||
use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SkimmedNode};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesResponse, SkimmedNode, SkimmedNodesWithMetadata,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_http_api_client::UserAgent;
|
||||
use nym_mixnet_contract_common::EpochRewardedSet;
|
||||
@@ -46,6 +48,46 @@ use crate::rpc::http_client;
|
||||
#[cfg(feature = "http-client")]
|
||||
use crate::{DirectSigningHttpRpcValidatorClient, HttpRpcClient, QueryHttpRpcValidatorClient};
|
||||
|
||||
// a simple helper macro to define to repeatedly call a paged query until a full response is constructed
|
||||
macro_rules! collect_paged_skimmed_v2 {
|
||||
( $self:ident, $f: ident ) => {{
|
||||
// unroll first loop iteration in order to obtain the metadata
|
||||
let mut page = 0;
|
||||
let res = $self
|
||||
.nym_api
|
||||
.$f(false, Some(page), None, $self.use_bincode)
|
||||
.await?;
|
||||
let mut nodes = res.nodes.data;
|
||||
let metadata = res.metadata;
|
||||
|
||||
if res.nodes.pagination.total == nodes.len() {
|
||||
return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
|
||||
}
|
||||
|
||||
page += 1;
|
||||
|
||||
loop {
|
||||
let mut res = $self
|
||||
.nym_api
|
||||
.$f(false, Some(page), None, $self.use_bincode)
|
||||
.await?;
|
||||
|
||||
if metadata != res.metadata {
|
||||
return Err(ValidatorClientError::InconsistentPagedMetadata);
|
||||
}
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
|
||||
}};
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
@@ -425,103 +467,67 @@ impl NymApiClient {
|
||||
|
||||
/// retrieve basic information for nodes are capable of operating as an entry gateway
|
||||
/// this includes legacy gateways and nym-nodes
|
||||
#[deprecated(note = "use get_all_basic_entry_assigned_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_entry_assigned_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut nodes = Vec::new();
|
||||
self.get_all_basic_entry_assigned_nodes_v2()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_basic_entry_assigned_nodes(false, Some(page), None, self.use_bincode)
|
||||
.await?;
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
pub async fn get_all_basic_entry_assigned_nodes_v2(
|
||||
&self,
|
||||
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
collect_paged_skimmed_v2!(self, get_basic_entry_assigned_nodes_v2)
|
||||
}
|
||||
|
||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_active_mixing_assigned_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut nodes = Vec::new();
|
||||
self.get_all_basic_active_mixing_assigned_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_basic_active_mixing_assigned_nodes(false, Some(page), None, self.use_bincode)
|
||||
.await?;
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
pub async fn get_all_basic_active_mixing_assigned_nodes_with_metadata(
|
||||
&self,
|
||||
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
collect_paged_skimmed_v2!(self, get_basic_active_mixing_assigned_nodes_v2)
|
||||
}
|
||||
|
||||
/// retrieve basic information for nodes are capable of operating as a mixnode
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[deprecated(note = "use get_all_basic_mixing_capable_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_mixing_capable_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut nodes = Vec::new();
|
||||
self.get_all_basic_mixing_capable_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_basic_mixing_capable_nodes(false, Some(page), None, self.use_bincode)
|
||||
.await?;
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
pub async fn get_all_basic_mixing_capable_nodes_with_metadata(
|
||||
&self,
|
||||
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
collect_paged_skimmed_v2!(self, get_basic_mixing_capable_nodes_v2)
|
||||
}
|
||||
|
||||
/// retrieve basic information for all bonded nodes on the network
|
||||
#[deprecated(note = "use get_all_basic_nodes_with_metadata instead")]
|
||||
pub async fn get_all_basic_nodes(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
|
||||
let mut page = 0;
|
||||
let mut nodes = Vec::new();
|
||||
self.get_all_basic_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
}
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_basic_nodes(false, Some(page), None, self.use_bincode)
|
||||
.await?;
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
pub async fn get_all_basic_nodes_with_metadata(
|
||||
&self,
|
||||
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
collect_paged_skimmed_v2!(self, get_basic_nodes_v2)
|
||||
}
|
||||
|
||||
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
|
||||
|
||||
@@ -22,6 +22,9 @@ pub enum ValidatorClientError {
|
||||
#[error("nyxd request failed: {0}")]
|
||||
NyxdError(#[from] crate::nyxd::error::NyxdError),
|
||||
|
||||
#[error("the response metadata has changed between pages")]
|
||||
InconsistentPagedMetadata,
|
||||
|
||||
#[error("No validator API url has been provided")]
|
||||
NoAPIUrlAvailable,
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ use nym_api_requests::ecash::models::{
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse,
|
||||
LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody, NymNodeDescription,
|
||||
PerformanceHistoryResponse, RewardedSetResponse,
|
||||
KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody,
|
||||
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse,
|
||||
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
|
||||
PaginatedCachedNodesResponseV2,
|
||||
};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
pub use nym_api_requests::{
|
||||
@@ -62,7 +63,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn health(&self) -> Result<ApiHealthResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::API_STATUS_ROUTES,
|
||||
routes::HEALTH,
|
||||
],
|
||||
@@ -75,7 +76,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn build_information(&self) -> Result<BinaryBuildInformationOwned, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::API_STATUS_ROUTES,
|
||||
routes::BUILD_INFORMATION,
|
||||
],
|
||||
@@ -87,7 +88,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[deprecated]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||
self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS)
|
||||
self.get_json(&[routes::V1_API_VERSION, routes::MIXNODES], NO_PARAMS)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODES,
|
||||
routes::DETAILED,
|
||||
@@ -111,7 +112,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_gateways_detailed(&self) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAYS,
|
||||
routes::DETAILED,
|
||||
@@ -128,7 +129,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAYS,
|
||||
routes::DETAILED_UNFILTERED,
|
||||
@@ -145,7 +146,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODES,
|
||||
routes::DETAILED_UNFILTERED,
|
||||
@@ -158,7 +159,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[deprecated]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_gateways(&self) -> Result<Vec<GatewayBond>, NymAPIError> {
|
||||
self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS)
|
||||
self.get_json(&[routes::V1_API_VERSION, routes::GATEWAYS], NO_PARAMS)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -166,7 +167,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_gateways_described(&self) -> Result<Vec<LegacyDescribedGateway>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED],
|
||||
&[routes::V1_API_VERSION, routes::GATEWAYS, routes::DESCRIBED],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -176,7 +177,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_mixnodes_described(&self) -> Result<Vec<LegacyDescribedMixNode>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED],
|
||||
&[routes::V1_API_VERSION, routes::MIXNODES, routes::DESCRIBED],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -201,7 +202,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_PERFORMANCE_HISTORY,
|
||||
&*node_id.to_string(),
|
||||
@@ -229,7 +230,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_DESCRIBED,
|
||||
],
|
||||
@@ -256,7 +257,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_BONDED,
|
||||
],
|
||||
@@ -270,7 +271,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"mixnodes",
|
||||
@@ -286,7 +287,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_basic_gateways(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"gateways",
|
||||
@@ -301,7 +302,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_REWARDED_SET,
|
||||
],
|
||||
@@ -312,6 +313,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
/// retrieve basic information for nodes are capable of operating as an entry gateway
|
||||
/// this includes legacy gateways and nym-nodes
|
||||
#[deprecated(note = "use get_basic_entry_assigned_nodes_v2")]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_entry_assigned_nodes(
|
||||
&self,
|
||||
@@ -319,7 +321,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -340,7 +342,49 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
"entry-gateways",
|
||||
"all",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// retrieve basic information for nodes are capable of operating as an entry gateway
|
||||
/// this includes legacy gateways and nym-nodes
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_entry_assigned_nodes_v2(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
if use_bincode {
|
||||
params.push(("output", "bincode".to_string()))
|
||||
}
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::V2_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
@@ -354,6 +398,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[deprecated(note = "use get_basic_active_mixing_assigned_nodes_v2")]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_active_mixing_assigned_nodes(
|
||||
&self,
|
||||
@@ -361,7 +406,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -382,7 +427,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
@@ -397,13 +442,13 @@ pub trait NymApiClientExt: ApiClient {
|
||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_mixing_capable_nodes(
|
||||
async fn get_basic_active_mixing_assigned_nodes_v2(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -424,7 +469,50 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V2_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
"mixnodes",
|
||||
"active",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[deprecated(note = "use get_basic_mixing_capable_nodes_v2")]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_mixing_capable_nodes(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
if use_bincode {
|
||||
params.push(("output", "bincode".to_string()))
|
||||
}
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
@@ -436,14 +524,16 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||
/// this includes legacy mixnodes and nym-nodes
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_nodes(
|
||||
async fn get_basic_mixing_capable_nodes_v2(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
@@ -464,7 +554,86 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V2_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
"mixnodes",
|
||||
"all",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[deprecated(note = "use get_basic_nodes_v2")]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_nodes(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
if use_bincode {
|
||||
params.push(("output", "bincode".to_string()))
|
||||
}
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_basic_nodes_v2(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
use_bincode: bool,
|
||||
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
if let Some(page) = page {
|
||||
params.push(("page", page.to_string()))
|
||||
}
|
||||
|
||||
if let Some(per_page) = per_page {
|
||||
params.push(("per_page", per_page.to_string()))
|
||||
}
|
||||
|
||||
if use_bincode {
|
||||
params.push(("output", "bincode".to_string()))
|
||||
}
|
||||
|
||||
self.get_response(
|
||||
&[
|
||||
routes::V2_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
@@ -478,7 +647,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE],
|
||||
&[routes::V1_API_VERSION, routes::MIXNODES, routes::ACTIVE],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -489,7 +658,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_active_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODES,
|
||||
routes::ACTIVE,
|
||||
@@ -504,7 +673,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_rewarded_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::MIXNODES, routes::REWARDED],
|
||||
&[routes::V1_API_VERSION, routes::MIXNODES, routes::REWARDED],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -518,7 +687,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<MixnodeStatusReportResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -537,7 +706,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<GatewayStatusReportResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
@@ -556,7 +725,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<MixnodeUptimeHistoryResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -575,7 +744,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<GatewayUptimeHistoryResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
@@ -593,7 +762,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODES,
|
||||
routes::REWARDED,
|
||||
@@ -614,7 +783,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
if let Some(since) = since {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
@@ -626,7 +795,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
} else {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
@@ -647,7 +816,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
if let Some(since) = since {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -659,7 +828,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
} else {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -679,7 +848,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<MixnodeStatusResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -698,7 +867,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<RewardEstimationResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -718,7 +887,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<RewardEstimationResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -738,7 +907,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<StakeSaturationResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -758,7 +927,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<nym_api_requests::models::InclusionProbabilityResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -776,7 +945,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<NodePerformanceResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_PERFORMANCE,
|
||||
&node_id.to_string(),
|
||||
@@ -792,7 +961,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<AnnotationResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_ANNOTATION,
|
||||
&node_id.to_string(),
|
||||
@@ -806,7 +975,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result<UptimeResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
@@ -821,7 +990,11 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_mixnodes_blacklisted(&self) -> Result<Vec<NodeId>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED],
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
routes::MIXNODES,
|
||||
routes::BLACKLISTED,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -831,7 +1004,11 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_gateways_blacklisted(&self) -> Result<Vec<IdentityKey>, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED],
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
routes::GATEWAYS,
|
||||
routes::BLACKLISTED,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -844,7 +1021,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<BlindedSignatureResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::ECASH_BLIND_SIGN,
|
||||
],
|
||||
@@ -861,7 +1038,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<EcashTicketVerificationResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::VERIFY_ECASH_TICKET,
|
||||
],
|
||||
@@ -878,7 +1055,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<EcashBatchTicketRedemptionResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::BATCH_REDEEM_ECASH_TICKETS,
|
||||
],
|
||||
@@ -903,7 +1080,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::PARTIAL_EXPIRATION_DATE_SIGNATURES,
|
||||
],
|
||||
@@ -924,7 +1101,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::PARTIAL_COIN_INDICES_SIGNATURES,
|
||||
],
|
||||
@@ -948,7 +1125,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::GLOBAL_EXPIRATION_DATE_SIGNATURES,
|
||||
],
|
||||
@@ -969,7 +1146,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::GLOBAL_COIN_INDICES_SIGNATURES,
|
||||
],
|
||||
@@ -989,7 +1166,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
};
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
ecash::MASTER_VERIFICATION_KEY,
|
||||
],
|
||||
@@ -1005,7 +1182,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<(), NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_REFRESH_DESCRIBED,
|
||||
],
|
||||
@@ -1022,7 +1199,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<IssuedTicketbooksForResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::ECASH_ISSUED_TICKETBOOKS_FOR,
|
||||
&expiration_date.to_string(),
|
||||
@@ -1039,7 +1216,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<IssuedTicketbooksForCountResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::ECASH_ISSUED_TICKETBOOKS_FOR_COUNT,
|
||||
&expiration_date.to_string(),
|
||||
@@ -1056,7 +1233,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<IssuedTicketbooksChallengeCommitmentResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT,
|
||||
],
|
||||
@@ -1073,7 +1250,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<IssuedTicketbooksDataResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
routes::ECASH_ROUTES,
|
||||
routes::ECASH_ISSUED_TICKETBOOKS_DATA,
|
||||
],
|
||||
@@ -1089,7 +1266,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
) -> Result<NodesByAddressesResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::V1_API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::nym_nodes::BY_ADDRESSES,
|
||||
@@ -1103,7 +1280,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_network_details(&self) -> Result<NymNetworkDetailsResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::NETWORK, routes::DETAILS],
|
||||
&[routes::V1_API_VERSION, routes::NETWORK, routes::DETAILS],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
@@ -1112,7 +1289,24 @@ pub trait NymApiClientExt: ApiClient {
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_chain_status(&self) -> Result<ChainStatusResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[routes::API_VERSION, routes::NETWORK, routes::CHAIN_STATUS],
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
routes::NETWORK,
|
||||
routes::CHAIN_STATUS,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_key_rotation_info(&self) -> Result<KeyRotationInfoResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
routes::EPOCH,
|
||||
routes::KEY_ROTATION_INFO,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_network_defaults::NYM_API_VERSION;
|
||||
|
||||
pub const API_VERSION: &str = NYM_API_VERSION;
|
||||
pub const V1_API_VERSION: &str = "v1";
|
||||
pub const V2_API_VERSION: &str = "v2";
|
||||
pub const MIXNODES: &str = "mixnodes";
|
||||
pub const GATEWAYS: &str = "gateways";
|
||||
pub const DESCRIBED: &str = "described";
|
||||
@@ -79,3 +78,11 @@ pub const SERVICE_PROVIDERS: &str = "services";
|
||||
pub const DETAILS: &str = "details";
|
||||
pub const CHAIN_STATUS: &str = "chain-status";
|
||||
pub const NETWORK: &str = "network";
|
||||
|
||||
pub const EPOCH: &str = "epoch";
|
||||
|
||||
pub use epoch_routes::*;
|
||||
pub mod epoch_routes {
|
||||
pub const CURRENT: &str = "current";
|
||||
pub const KEY_ROTATION_INFO: &str = "key-rotation-info";
|
||||
}
|
||||
|
||||
+29
-9
@@ -12,8 +12,8 @@ use nym_mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, Preassi
|
||||
use nym_mixnet_contract_common::nym_node::{
|
||||
EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse,
|
||||
NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse,
|
||||
PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse,
|
||||
StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode,
|
||||
PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, RewardedSetMetadata, Role,
|
||||
RolesMetadataResponse, StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode,
|
||||
};
|
||||
use nym_mixnet_contract_common::reward_params::WorkFactor;
|
||||
use nym_mixnet_contract_common::{
|
||||
@@ -28,12 +28,12 @@ use nym_mixnet_contract_common::{
|
||||
ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse,
|
||||
CurrentNymNodeVersionResponse, Delegation, EpochEventId, EpochRewardedSet, EpochStatus,
|
||||
GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, HistoricalNymNodeVersionEntry,
|
||||
IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, MixNodeDetails,
|
||||
MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeId,
|
||||
NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, NymNodeVersionHistoryResponse,
|
||||
PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse,
|
||||
PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent,
|
||||
PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent,
|
||||
IdentityKey, IdentityKeyRef, IntervalEventId, KeyRotationIdResponse, KeyRotationState,
|
||||
MixNodeBond, MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse,
|
||||
MixnodeDetailsResponse, NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails,
|
||||
NymNodeVersionHistoryResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse,
|
||||
PagedGatewayResponse, PagedMixnodeBondsResponse, PagedNodeDelegationsResponse,
|
||||
PendingEpochEvent, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent,
|
||||
PendingIntervalEventResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg,
|
||||
RewardedSet, UnbondedMixnode,
|
||||
};
|
||||
@@ -546,6 +546,16 @@ pub trait MixnetQueryClient {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_key_rotation_state(&self) -> Result<KeyRotationState, NyxdError> {
|
||||
self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationState {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_key_rotation_id(&self) -> Result<KeyRotationIdResponse, NyxdError> {
|
||||
self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationId {})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// extension trait to the query client to deal with the paged queries
|
||||
@@ -673,12 +683,20 @@ pub trait MixnetQueryClientExt: MixnetQueryClient {
|
||||
async fn get_rewarded_set(&self) -> Result<EpochRewardedSet, NyxdError> {
|
||||
let error_response = |message| Err(NyxdError::extension_query_failure("mixnet", message));
|
||||
|
||||
// bypass for catch 22 for fresh contracts. we can't refresh cache because there's no rewarded set,
|
||||
// but we can't set the rewarded set because we didn't refresh the cache
|
||||
let metadata = self.get_rewarded_set_metadata().await?;
|
||||
if !metadata.metadata.fully_assigned {
|
||||
|
||||
let is_default = metadata.metadata == RewardedSetMetadata::default();
|
||||
if !metadata.metadata.fully_assigned && !is_default {
|
||||
return error_response("the rewarded set hasn't been fully assigned for this epoch");
|
||||
}
|
||||
let expected_epoch_id = metadata.metadata.epoch_id;
|
||||
|
||||
if is_default {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
|
||||
// if we have to query those things more frequently, we could do it concurrently,
|
||||
// but as it stands now, it happens so infrequently it might as well be sequential
|
||||
let entry = self.get_role_assignment(Role::EntryGateway).await?;
|
||||
@@ -955,6 +973,8 @@ mod tests {
|
||||
QueryMsg::GetNymNodeVersionHistory { limit, start_after } => client
|
||||
.get_nym_node_version_history_paged(start_after, limit)
|
||||
.ignore(),
|
||||
QueryMsg::GetKeyRotationState {} => client.get_key_rotation_state().ignore(),
|
||||
QueryMsg::GetKeyRotationId {} => client.get_key_rotation_id().ignore(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,14 @@ impl NyxdClient<HttpClient> {
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn connect_to_default_env<U>(endpoint: U) -> Result<QueryHttpRpcNyxdClient, NyxdError>
|
||||
where
|
||||
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
|
||||
{
|
||||
let config = Config::try_from_nym_network_details(&NymNetworkDetails::new_from_env())?;
|
||||
Self::connect(config, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
impl NyxdClient<ReqwestRpcClient> {
|
||||
|
||||
@@ -157,6 +157,7 @@ pub async fn generate(args: Args) {
|
||||
minimum: args.minimum_interval_operating_cost.amount.into(),
|
||||
maximum: args.maximum_interval_operating_cost.amount.into(),
|
||||
},
|
||||
key_validity_in_epochs: None,
|
||||
};
|
||||
|
||||
debug!("instantiate_msg: {:?}", instantiate_msg);
|
||||
|
||||
@@ -40,3 +40,6 @@ contract-testing = []
|
||||
utoipa = ["dep:utoipa"]
|
||||
schema = ["cw2"]
|
||||
generate-ts = ['ts-rs']
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -193,6 +193,9 @@ pub enum MixnetContractError {
|
||||
#[error("attempted to perform the operation with 0 coins. This is not allowed")]
|
||||
ZeroCoinAmount,
|
||||
|
||||
#[error("key rotation validity below minimum value")]
|
||||
TooShortRotationInterval,
|
||||
|
||||
#[error("this validator ({current_validator}) is not the one responsible for advancing this epoch. It's responsibility of {chosen_validator}.")]
|
||||
RewardingValidatorMismatch {
|
||||
current_validator: Addr,
|
||||
|
||||
@@ -358,7 +358,7 @@ impl Interval {
|
||||
self.total_elapsed_epochs
|
||||
}
|
||||
|
||||
pub const fn current_epoch_absolute_id(&self) -> u32 {
|
||||
pub const fn current_epoch_absolute_id(&self) -> EpochId {
|
||||
// since we count epochs starting from 0, if n epochs have elapsed, the current one has absolute id of n
|
||||
self.total_elapsed_epochs
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::EpochId;
|
||||
use cosmwasm_schema::cw_serde;
|
||||
|
||||
pub type KeyRotationId = u32;
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Copy)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct KeyRotationState {
|
||||
/// Defines how long each key rotation is valid for (in terms of epochs)
|
||||
pub validity_epochs: u32,
|
||||
|
||||
/// Records the initial epoch_id when the key rotation has been introduced (0 for fresh contracts).
|
||||
/// It is used for determining when rotation is meant to advance.
|
||||
#[cfg_attr(feature = "utoipa", schema(value_type = u32))]
|
||||
pub initial_epoch_id: EpochId,
|
||||
}
|
||||
|
||||
impl KeyRotationState {
|
||||
pub fn key_rotation_id(&self, current_epoch_id: EpochId) -> KeyRotationId {
|
||||
let diff = current_epoch_id.saturating_sub(self.initial_epoch_id);
|
||||
diff / self.validity_epochs
|
||||
}
|
||||
|
||||
pub fn next_rotation_starting_epoch_id(&self, current_epoch_id: EpochId) -> EpochId {
|
||||
self.current_rotation_starting_epoch_id(current_epoch_id) + self.validity_epochs
|
||||
}
|
||||
|
||||
pub fn current_rotation_starting_epoch_id(&self, current_epoch_id: EpochId) -> EpochId {
|
||||
let current_rotation_id = self.key_rotation_id(current_epoch_id);
|
||||
|
||||
self.initial_epoch_id + self.validity_epochs * current_rotation_id
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct KeyRotationIdResponse {
|
||||
pub rotation_id: KeyRotationId,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn key_rotation_id() {
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(0, state.key_rotation_id(0));
|
||||
assert_eq!(0, state.key_rotation_id(23));
|
||||
assert_eq!(1, state.key_rotation_id(24));
|
||||
assert_eq!(1, state.key_rotation_id(47));
|
||||
assert_eq!(2, state.key_rotation_id(48));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 12,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(0, state.key_rotation_id(0));
|
||||
assert_eq!(0, state.key_rotation_id(11));
|
||||
assert_eq!(1, state.key_rotation_id(12));
|
||||
assert_eq!(1, state.key_rotation_id(23));
|
||||
assert_eq!(2, state.key_rotation_id(24));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 10000,
|
||||
};
|
||||
assert_eq!(0, state.key_rotation_id(123));
|
||||
assert_eq!(0, state.key_rotation_id(10000));
|
||||
assert_eq!(0, state.key_rotation_id(10001));
|
||||
assert_eq!(0, state.key_rotation_id(10023));
|
||||
assert_eq!(1, state.key_rotation_id(10024));
|
||||
assert_eq!(1, state.key_rotation_id(10047));
|
||||
assert_eq!(2, state.key_rotation_id(10048));
|
||||
assert_eq!(2, state.key_rotation_id(10060));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_rotation_starting_epoch_id() {
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(24, state.next_rotation_starting_epoch_id(0));
|
||||
assert_eq!(24, state.next_rotation_starting_epoch_id(23));
|
||||
assert_eq!(48, state.next_rotation_starting_epoch_id(24));
|
||||
assert_eq!(48, state.next_rotation_starting_epoch_id(47));
|
||||
assert_eq!(72, state.next_rotation_starting_epoch_id(48));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 12,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(12, state.next_rotation_starting_epoch_id(0));
|
||||
assert_eq!(12, state.next_rotation_starting_epoch_id(11));
|
||||
assert_eq!(24, state.next_rotation_starting_epoch_id(12));
|
||||
assert_eq!(24, state.next_rotation_starting_epoch_id(23));
|
||||
assert_eq!(36, state.next_rotation_starting_epoch_id(24));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 10000,
|
||||
};
|
||||
assert_eq!(10024, state.next_rotation_starting_epoch_id(123));
|
||||
assert_eq!(10024, state.next_rotation_starting_epoch_id(10000));
|
||||
assert_eq!(10024, state.next_rotation_starting_epoch_id(10001));
|
||||
assert_eq!(10024, state.next_rotation_starting_epoch_id(10023));
|
||||
assert_eq!(10048, state.next_rotation_starting_epoch_id(10024));
|
||||
assert_eq!(10048, state.next_rotation_starting_epoch_id(10047));
|
||||
assert_eq!(10072, state.next_rotation_starting_epoch_id(10048));
|
||||
assert_eq!(10072, state.next_rotation_starting_epoch_id(10060));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_rotation_starting_epoch_id() {
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(0, state.current_rotation_starting_epoch_id(0));
|
||||
assert_eq!(0, state.current_rotation_starting_epoch_id(23));
|
||||
assert_eq!(24, state.current_rotation_starting_epoch_id(24));
|
||||
assert_eq!(24, state.current_rotation_starting_epoch_id(47));
|
||||
assert_eq!(48, state.current_rotation_starting_epoch_id(48));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 12,
|
||||
initial_epoch_id: 0,
|
||||
};
|
||||
assert_eq!(0, state.current_rotation_starting_epoch_id(0));
|
||||
assert_eq!(0, state.current_rotation_starting_epoch_id(11));
|
||||
assert_eq!(12, state.current_rotation_starting_epoch_id(12));
|
||||
assert_eq!(12, state.current_rotation_starting_epoch_id(23));
|
||||
assert_eq!(24, state.current_rotation_starting_epoch_id(24));
|
||||
|
||||
let state = KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: 10000,
|
||||
};
|
||||
assert_eq!(10000, state.current_rotation_starting_epoch_id(123));
|
||||
assert_eq!(10000, state.current_rotation_starting_epoch_id(10000));
|
||||
assert_eq!(10000, state.current_rotation_starting_epoch_id(10001));
|
||||
assert_eq!(10000, state.current_rotation_starting_epoch_id(10023));
|
||||
assert_eq!(10024, state.current_rotation_starting_epoch_id(10024));
|
||||
assert_eq!(10024, state.current_rotation_starting_epoch_id(10047));
|
||||
assert_eq!(10048, state.current_rotation_starting_epoch_id(10048));
|
||||
assert_eq!(10048, state.current_rotation_starting_epoch_id(10060));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
#![warn(clippy::todo)]
|
||||
|
||||
mod config_score;
|
||||
pub mod constants;
|
||||
pub mod delegation;
|
||||
@@ -13,6 +9,7 @@ pub mod events;
|
||||
pub mod gateway;
|
||||
pub mod helpers;
|
||||
pub mod interval;
|
||||
pub mod key_rotation;
|
||||
pub mod mixnode;
|
||||
pub mod msg;
|
||||
pub mod nym_node;
|
||||
@@ -37,6 +34,7 @@ pub use gateway::{
|
||||
pub use interval::{
|
||||
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
|
||||
};
|
||||
pub use key_rotation::*;
|
||||
pub use mixnode::{
|
||||
LegacyMixLayer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeDetails,
|
||||
MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeCostParams,
|
||||
|
||||
@@ -170,6 +170,11 @@ impl NodeRewarding {
|
||||
}
|
||||
}
|
||||
|
||||
// we panic here as opposed to returning an error as this is undefined behaviour,
|
||||
// because the pledge amount has decreased (i.e. slashing has occurred) which
|
||||
// should not be possible under any situation. at this point we don't know how many other things
|
||||
// might have failed so we have to bail
|
||||
#[allow(clippy::panic)]
|
||||
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> StdResult<Decimal> {
|
||||
let initial_dec = original_pledge.amount.into_base_decimal()?;
|
||||
if initial_dec > self.operator {
|
||||
@@ -189,6 +194,11 @@ impl NodeRewarding {
|
||||
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
|
||||
}
|
||||
|
||||
// we panic here as opposed to returning an error as this is undefined behaviour,
|
||||
// because the pledge amount has decreased (i.e. slashing has occurred) which
|
||||
// should not be possible under any situation. at this point we don't know how many other things
|
||||
// might have failed so we have to bail
|
||||
#[allow(clippy::panic)]
|
||||
pub fn withdraw_operator_reward(
|
||||
&mut self,
|
||||
original_pledge: &Coin,
|
||||
|
||||
@@ -35,6 +35,7 @@ use crate::{
|
||||
PreassignedGatewayIdsResponse,
|
||||
},
|
||||
interval::{CurrentIntervalResponse, EpochStatus},
|
||||
key_rotation::{KeyRotationIdResponse, KeyRotationState},
|
||||
mixnode::{
|
||||
MixOwnershipResponse, MixStakeSaturationResponse, MixnodeDetailsByIdentityResponse,
|
||||
MixnodeDetailsResponse, MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse,
|
||||
@@ -81,6 +82,18 @@ pub struct InstantiateMsg {
|
||||
|
||||
#[serde(default)]
|
||||
pub interval_operating_cost: OperatingCostRange,
|
||||
|
||||
#[serde(default)]
|
||||
pub key_validity_in_epochs: Option<u32>,
|
||||
}
|
||||
|
||||
impl InstantiateMsg {
|
||||
// needs to give us enough time to pre-announce key for following epoch
|
||||
// and have an overlap with the preceding epoch
|
||||
pub const MIN_KEY_ROTATION_VALIDITY: u32 = 3;
|
||||
pub fn key_validity_in_epochs(&self) -> u32 {
|
||||
self.key_validity_in_epochs.unwrap_or(24)
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
@@ -857,6 +870,15 @@ pub enum QueryMsg {
|
||||
/// Cosmos address used for the query of the signing nonce.
|
||||
address: String,
|
||||
},
|
||||
|
||||
// sphinx key rotation-related
|
||||
#[cfg_attr(feature = "schema", returns(KeyRotationState))]
|
||||
/// Gets the current state config of the key rotation (i.e. starting epoch id and validity duration)
|
||||
GetKeyRotationState {},
|
||||
|
||||
/// Gets the current key rotation id
|
||||
#[cfg_attr(feature = "schema", returns(KeyRotationIdResponse))]
|
||||
GetKeyRotationId {},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
|
||||
@@ -151,6 +151,9 @@ impl Simulator {
|
||||
}
|
||||
}
|
||||
|
||||
// this code is not meant to be used in production systems, only in tests
|
||||
// so a panic due to inconsistent arguments is fine
|
||||
#[allow(clippy::panic)]
|
||||
pub fn simulate_epoch(
|
||||
&mut self,
|
||||
node_params: &BTreeMap<NodeId, NodeRewardingParameters>,
|
||||
|
||||
@@ -19,7 +19,7 @@ pub use shared_key::{
|
||||
SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey,
|
||||
};
|
||||
|
||||
pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION;
|
||||
pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION;
|
||||
|
||||
/// Defines the current version of the communication protocol between gateway and clients.
|
||||
/// It has to be incremented for any breaking change.
|
||||
@@ -28,10 +28,12 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION;
|
||||
// 2 - changes to client credentials structure
|
||||
// 3 - change to AES-GCM-SIV and non-zero IVs
|
||||
// 4 - introduction of v2 authentication protocol to prevent reply attacks
|
||||
// 5 - add key rotation information to the serialised mix packet
|
||||
pub const INITIAL_PROTOCOL_VERSION: u8 = 1;
|
||||
pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2;
|
||||
pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3;
|
||||
pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4;
|
||||
pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: u8 = 5;
|
||||
|
||||
// TODO: could using `Mac` trait here for OutputSize backfire?
|
||||
// Should hmac itself be exposed, imported and used instead?
|
||||
@@ -40,6 +42,7 @@ pub type LegacyGatewayMacSize = <GatewayIntegrityHmacAlgorithm as OutputSizeUser
|
||||
pub trait GatewayProtocolVersionExt {
|
||||
fn supports_aes256_gcm_siv(&self) -> bool;
|
||||
fn supports_authenticate_v2(&self) -> bool;
|
||||
fn supports_key_rotation_packet(&self) -> bool;
|
||||
}
|
||||
|
||||
impl GatewayProtocolVersionExt for Option<u8> {
|
||||
@@ -52,4 +55,9 @@ impl GatewayProtocolVersionExt for Option<u8> {
|
||||
let Some(protocol) = *self else { return false };
|
||||
protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION
|
||||
}
|
||||
|
||||
fn supports_key_rotation_packet(&self) -> bool {
|
||||
let Some(protocol) = *self else { return false };
|
||||
protocol >= EMBEDDED_KEY_ROTATION_INFO_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ use tungstenite::Message;
|
||||
#[non_exhaustive]
|
||||
pub enum BinaryRequest {
|
||||
ForwardSphinx { packet: MixPacket },
|
||||
|
||||
// identical to `ForwardSphinx`, but also contains information about sphinx key rotation used
|
||||
ForwardSphinxV2 { packet: MixPacket },
|
||||
}
|
||||
|
||||
#[repr(u8)]
|
||||
@@ -18,6 +21,9 @@ pub enum BinaryRequest {
|
||||
#[non_exhaustive]
|
||||
pub enum BinaryRequestKind {
|
||||
ForwardSphinx = 1,
|
||||
|
||||
// identical to `ForwardSphinx`, but also contains information about sphinx key rotation used
|
||||
ForwardSphinxV2 = 2,
|
||||
}
|
||||
|
||||
// Right now the only valid `BinaryRequest` is a request to forward a sphinx packet.
|
||||
@@ -29,6 +35,7 @@ impl BinaryRequest {
|
||||
pub fn kind(&self) -> BinaryRequestKind {
|
||||
match self {
|
||||
BinaryRequest::ForwardSphinx { .. } => BinaryRequestKind::ForwardSphinx,
|
||||
BinaryRequest::ForwardSphinxV2 { .. } => BinaryRequestKind::ForwardSphinxV2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +45,13 @@ impl BinaryRequest {
|
||||
) -> Result<Self, GatewayRequestsError> {
|
||||
match kind {
|
||||
BinaryRequestKind::ForwardSphinx => {
|
||||
let packet = MixPacket::try_from_bytes(plaintext)?;
|
||||
let packet = MixPacket::try_from_v1_bytes(plaintext)?;
|
||||
Ok(BinaryRequest::ForwardSphinx { packet })
|
||||
}
|
||||
BinaryRequestKind::ForwardSphinxV2 => {
|
||||
let packet = MixPacket::try_from_v2_bytes(plaintext)?;
|
||||
Ok(BinaryRequest::ForwardSphinxV2 { packet })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +69,8 @@ impl BinaryRequest {
|
||||
let kind = self.kind();
|
||||
|
||||
let plaintext = match self {
|
||||
BinaryRequest::ForwardSphinx { packet } => packet.into_bytes()?,
|
||||
BinaryRequest::ForwardSphinx { packet } => packet.into_v1_bytes()?,
|
||||
BinaryRequest::ForwardSphinxV2 { packet } => packet.into_v2_bytes()?,
|
||||
};
|
||||
|
||||
BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key)
|
||||
@@ -70,7 +82,9 @@ impl BinaryRequest {
|
||||
) -> Result<Message, GatewayRequestsError> {
|
||||
// all variants are currently encrypted
|
||||
let blob = match self {
|
||||
BinaryRequest::ForwardSphinx { .. } => self.into_encrypted_tagged_bytes(shared_key)?,
|
||||
BinaryRequest::ForwardSphinx { .. } | BinaryRequest::ForwardSphinxV2 { .. } => {
|
||||
self.into_encrypted_tagged_bytes(shared_key)?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Message::Binary(blob))
|
||||
|
||||
@@ -601,6 +601,21 @@ impl Client {
|
||||
self.base_urls = new_urls
|
||||
}
|
||||
|
||||
/// Create new instance of `Client` using the provided base url and existing client config
|
||||
pub fn clone_with_new_url(&self, new_url: Url) -> Self {
|
||||
Client {
|
||||
base_urls: vec![new_url],
|
||||
current_idx: Arc::new(Default::default()),
|
||||
reqwest_client: self.reqwest_client.clone(),
|
||||
|
||||
front: self.front.clone(),
|
||||
retry_limit: self.retry_limit,
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
request_timeout: self.request_timeout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the currently configured host that this client uses when sending API requests.
|
||||
pub fn current_url(&self) -> &Url {
|
||||
&self.base_urls[self.current_idx.load(std::sync::atomic::Ordering::Relaxed)]
|
||||
|
||||
@@ -30,6 +30,10 @@ impl<T> Bincode<T> {
|
||||
self.0.headers.insert(name, value.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Bincode<U> {
|
||||
Bincode(self.0.map(op))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for Bincode<T>
|
||||
|
||||
@@ -32,6 +32,10 @@ impl<T> Json<T> {
|
||||
self.0.headers.insert(name, value.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Json<U> {
|
||||
Json(self.0.map(op))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for Json<T>
|
||||
|
||||
@@ -14,11 +14,10 @@ pub mod bincode;
|
||||
pub mod json;
|
||||
pub mod yaml;
|
||||
|
||||
pub use bincode::Bincode;
|
||||
pub use json::Json;
|
||||
pub use yaml::Yaml;
|
||||
|
||||
pub use bincode::Bincode;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct ResponseWrapper<T> {
|
||||
data: T,
|
||||
@@ -33,6 +32,13 @@ impl<T> ResponseWrapper<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> ResponseWrapper<U> {
|
||||
ResponseWrapper {
|
||||
data: op(self.data),
|
||||
headers: self.headers,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn with_header(
|
||||
mut self,
|
||||
@@ -60,6 +66,14 @@ impl<T> FormattedResponse<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> FormattedResponse<U> {
|
||||
match self {
|
||||
FormattedResponse::Json(inner) => FormattedResponse::Json(inner.map(op)),
|
||||
FormattedResponse::Yaml(inner) => FormattedResponse::Yaml(inner.map(op)),
|
||||
FormattedResponse::Bincode(inner) => FormattedResponse::Bincode(inner.map(op)),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_header(
|
||||
self,
|
||||
|
||||
@@ -30,6 +30,10 @@ impl<T> Yaml<T> {
|
||||
self.0.headers.insert(name, value.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Yaml<U> {
|
||||
Yaml(self.0.map(op))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for Yaml<T>
|
||||
|
||||
@@ -10,7 +10,6 @@ repository = { workspace = true }
|
||||
[dependencies]
|
||||
rand = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -22,7 +21,7 @@ nym-sphinx-types = { path = "../types" }
|
||||
nym-topology = { path = "../../topology" }
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
|
||||
version = "0.2.95"
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = { workspace = true }
|
||||
|
||||
@@ -6,4 +6,4 @@ pub mod reply_surb;
|
||||
pub mod requests;
|
||||
|
||||
pub use encryption_key::{SurbEncryptionKey, SurbEncryptionKeySize};
|
||||
pub use reply_surb::{ReplySurb, ReplySurbError};
|
||||
pub use reply_surb::{ReplySurb, ReplySurbError, ReplySurbWithKeyRotation};
|
||||
|
||||
@@ -8,16 +8,13 @@ use nym_sphinx_addressing::nodes::{
|
||||
NymNodeRoutingAddress, NymNodeRoutingAddressError, MAX_NODE_ADDRESS_UNPADDED_LEN,
|
||||
};
|
||||
use nym_sphinx_params::packet_sizes::PacketSize;
|
||||
use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm};
|
||||
use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, SphinxKeyRotation};
|
||||
use nym_sphinx_types::{
|
||||
NymPacket, SURBMaterial, SphinxError, HEADER_SIZE, NODE_ADDRESS_LENGTH, SURB,
|
||||
X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION,
|
||||
};
|
||||
use nym_topology::{NymRouteProvider, NymTopologyError};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::de::{Error as SerdeError, Visitor};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt::{self, Formatter};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -48,44 +45,6 @@ pub struct ReplySurb {
|
||||
pub(crate) encryption_key: SurbEncryptionKey,
|
||||
}
|
||||
|
||||
// Serialize + Deserialize is not really used anymore (it was for a CBOR experiment)
|
||||
// however, if we decided we needed it again, it's already here
|
||||
impl Serialize for ReplySurb {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_bytes(&self.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ReplySurb {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ReplySurbVisitor;
|
||||
|
||||
impl Visitor<'_> for ReplySurbVisitor {
|
||||
type Value = ReplySurb;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(formatter, "A replySURB must contain a valid symmetric encryption key and a correctly formed sphinx header")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, bytes: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: SerdeError,
|
||||
{
|
||||
ReplySurb::from_bytes(bytes)
|
||||
.map_err(|_| SerdeError::invalid_length(bytes.len(), &self))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_bytes(ReplySurbVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplySurb {
|
||||
/// base overhead of a reply surb that exists regardless of type or number of key materials.
|
||||
pub(crate) const BASE_OVERHEAD: usize =
|
||||
@@ -123,6 +82,7 @@ impl ReplySurb {
|
||||
Ok(ReplySurb {
|
||||
surb: surb_material.construct_SURB().unwrap(),
|
||||
encryption_key: SurbEncryptionKey::new(rng),
|
||||
// used_key_rotation: SphinxKeyRotation::from(topology.current_key_rotation()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -198,8 +158,75 @@ impl ReplySurb {
|
||||
.use_surb(message_bytes, packet_size.payload_size())
|
||||
.expect("this error indicates inconsistent message length checking - it shouldn't have happened!");
|
||||
|
||||
let first_hop_address = NymNodeRoutingAddress::try_from(first_hop).unwrap();
|
||||
let first_hop_address = NymNodeRoutingAddress::try_from(first_hop)?;
|
||||
|
||||
Ok((NymPacket::Sphinx(packet), first_hop_address))
|
||||
}
|
||||
|
||||
pub fn to_legacy(self) -> ReplySurbWithKeyRotation {
|
||||
self.with_key_rotation(SphinxKeyRotation::Unknown)
|
||||
}
|
||||
|
||||
pub fn with_key_rotation(self, key_rotation: SphinxKeyRotation) -> ReplySurbWithKeyRotation {
|
||||
ReplySurbWithKeyRotation {
|
||||
inner: self,
|
||||
key_rotation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ReplySurbWithKeyRotation {
|
||||
pub(crate) inner: ReplySurb,
|
||||
pub(crate) key_rotation: SphinxKeyRotation,
|
||||
}
|
||||
|
||||
impl ReplySurbWithKeyRotation {
|
||||
pub fn encryption_key(&self) -> &SurbEncryptionKey {
|
||||
self.inner.encryption_key()
|
||||
}
|
||||
|
||||
pub fn inner_reply_surb(&self) -> &ReplySurb {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn key_rotation(&self) -> SphinxKeyRotation {
|
||||
self.key_rotation
|
||||
}
|
||||
|
||||
pub fn apply_surb<M: AsRef<[u8]>>(
|
||||
self,
|
||||
message: M,
|
||||
packet_size: PacketSize,
|
||||
_packet_type: PacketType,
|
||||
) -> Result<AppliedReplySurb, ReplySurbError> {
|
||||
let (packet, first_hop_address) =
|
||||
self.inner.apply_surb(message, packet_size, _packet_type)?;
|
||||
|
||||
Ok(AppliedReplySurb {
|
||||
packet,
|
||||
first_hop_address,
|
||||
key_rotation: self.key_rotation,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AppliedReplySurb {
|
||||
pub(crate) packet: NymPacket,
|
||||
pub(crate) first_hop_address: NymNodeRoutingAddress,
|
||||
pub(crate) key_rotation: SphinxKeyRotation,
|
||||
}
|
||||
|
||||
impl AppliedReplySurb {
|
||||
pub fn first_hop_address(&self) -> NymNodeRoutingAddress {
|
||||
self.first_hop_address
|
||||
}
|
||||
|
||||
pub fn key_rotation(&self) -> SphinxKeyRotation {
|
||||
self.key_rotation
|
||||
}
|
||||
|
||||
pub fn into_packet(self) -> NymPacket {
|
||||
self.packet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{ReplySurb, ReplySurbError};
|
||||
use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1};
|
||||
use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2};
|
||||
use crate::{ReplySurbError, ReplySurbWithKeyRotation};
|
||||
use nym_sphinx_addressing::clients::{Recipient, RecipientFormattingError};
|
||||
use nym_sphinx_params::key_rotation::InvalidSphinxKeyRotation;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::mem;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1};
|
||||
use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
@@ -84,7 +84,7 @@ impl AnonymousSenderTag {
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum InvalidReplyRequestError {
|
||||
#[error("Did not provide sufficient number of bytes to deserialize a valid request")]
|
||||
#[error("Did not provide sufficient number of bytes to deserialise a valid request")]
|
||||
RequestTooShortToDeserialize,
|
||||
|
||||
#[error("{received} is not a valid content tag for a repliable message")]
|
||||
@@ -93,10 +93,13 @@ pub enum InvalidReplyRequestError {
|
||||
#[error("{received} is not a valid content tag for a reply message")]
|
||||
InvalidReplyContentTag { received: u8 },
|
||||
|
||||
#[error("failed to deserialize recipient information - {0}")]
|
||||
#[error("failed to deserialise sphinx key rotation details: {0}")]
|
||||
MalformedSphinxKeyRotation(#[from] InvalidSphinxKeyRotation),
|
||||
|
||||
#[error("failed to deserialise recipient information: {0}")]
|
||||
MalformedRecipient(#[from] RecipientFormattingError),
|
||||
|
||||
#[error("failed to deserialize replySURB - {0}")]
|
||||
#[error("failed to deserialise replySURB: {0}")]
|
||||
MalformedReplySurb(#[from] ReplySurbError),
|
||||
}
|
||||
|
||||
@@ -136,7 +139,7 @@ impl RepliableMessage {
|
||||
use_legacy_surb_format: bool,
|
||||
data: Vec<u8>,
|
||||
sender_tag: AnonymousSenderTag,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
) -> Self {
|
||||
let content = if use_legacy_surb_format {
|
||||
RepliableMessageContent::Data(DataV1 {
|
||||
@@ -159,7 +162,7 @@ impl RepliableMessage {
|
||||
pub fn new_additional_surbs(
|
||||
use_legacy_surb_format: bool,
|
||||
sender_tag: AnonymousSenderTag,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
) -> Self {
|
||||
let content = if use_legacy_surb_format {
|
||||
RepliableMessageContent::AdditionalSurbs(AdditionalSurbsV1 { reply_surbs })
|
||||
@@ -484,9 +487,10 @@ mod tests {
|
||||
use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1};
|
||||
use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2};
|
||||
use crate::requests::{AnonymousSenderTag, RepliableMessageContent, ReplyMessageContent};
|
||||
use crate::{ReplySurb, SurbEncryptionKey};
|
||||
use crate::{ReplySurb, ReplySurbWithKeyRotation, SurbEncryptionKey};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_sphinx_addressing::clients::Recipient;
|
||||
use nym_sphinx_params::SphinxKeyRotation;
|
||||
use nym_sphinx_types::{
|
||||
Delay, Destination, DestinationAddressBytes, Node, NodeAddressBytes, PrivateKey,
|
||||
SURBMaterial, NODE_ADDRESS_LENGTH, X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION,
|
||||
@@ -571,10 +575,12 @@ mod tests {
|
||||
n: usize,
|
||||
legacy: bool,
|
||||
hops: u8,
|
||||
) -> Vec<ReplySurb> {
|
||||
) -> Vec<ReplySurbWithKeyRotation> {
|
||||
let mut surbs = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
surbs.push(reply_surb(rng, legacy, hops))
|
||||
surbs.push(
|
||||
reply_surb(rng, legacy, hops).with_key_rotation(SphinxKeyRotation::Unknown),
|
||||
)
|
||||
}
|
||||
surbs
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::requests::InvalidReplyRequestError;
|
||||
use crate::ReplySurb;
|
||||
use crate::{ReplySurb, ReplySurbWithKeyRotation};
|
||||
use nym_sphinx_types::PAYLOAD_KEY_SIZE;
|
||||
use std::fmt::Display;
|
||||
use std::mem;
|
||||
@@ -14,10 +14,10 @@ const fn v1_reply_surb_serialised_len() -> usize {
|
||||
ReplySurb::BASE_OVERHEAD + 4 * PAYLOAD_KEY_SIZE
|
||||
}
|
||||
|
||||
fn v1_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize {
|
||||
fn v1_reply_surbs_serialised_len(surbs: &[ReplySurbWithKeyRotation]) -> usize {
|
||||
// sanity checks; this should probably be removed later on
|
||||
if let Some(reply_surb) = surbs.first() {
|
||||
if reply_surb.surb.uses_key_seeds() {
|
||||
if reply_surb.inner.surb.uses_key_seeds() {
|
||||
error!("using v1 surbs encoding with updated structure - the surbs will be unusable")
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ fn v1_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize {
|
||||
// NUM_SURBS (u32) || SURB_DATA
|
||||
fn recover_reply_surbs_v1(
|
||||
bytes: &[u8],
|
||||
) -> Result<(Vec<ReplySurb>, usize), InvalidReplyRequestError> {
|
||||
) -> Result<(Vec<ReplySurbWithKeyRotation>, usize), InvalidReplyRequestError> {
|
||||
let mut consumed = mem::size_of::<u32>();
|
||||
if bytes.len() < consumed {
|
||||
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
|
||||
@@ -45,7 +45,7 @@ fn recover_reply_surbs_v1(
|
||||
let mut reply_surbs = Vec::with_capacity(num_surbs as usize);
|
||||
for _ in 0..num_surbs as usize {
|
||||
let surb_bytes = &bytes[consumed..consumed + surb_size];
|
||||
let reply_surb = ReplySurb::from_bytes(surb_bytes)?;
|
||||
let reply_surb = ReplySurb::from_bytes(surb_bytes)?.to_legacy();
|
||||
reply_surbs.push(reply_surb);
|
||||
|
||||
consumed += surb_size;
|
||||
@@ -55,19 +55,21 @@ fn recover_reply_surbs_v1(
|
||||
}
|
||||
|
||||
// length (u32) prefixed reply surbs with legacy serialisation of 4 hops and full payload keys attached
|
||||
fn reply_surbs_bytes_v1(reply_surbs: &[ReplySurb]) -> impl Iterator<Item = u8> + use<'_> {
|
||||
fn reply_surbs_bytes_v1(
|
||||
reply_surbs: &[ReplySurbWithKeyRotation],
|
||||
) -> impl Iterator<Item = u8> + use<'_> {
|
||||
let num_surbs = reply_surbs.len() as u32;
|
||||
|
||||
num_surbs
|
||||
.to_be_bytes()
|
||||
.into_iter()
|
||||
.chain(reply_surbs.iter().flat_map(|s| s.to_bytes()))
|
||||
.chain(reply_surbs.iter().flat_map(|s| s.inner.to_bytes()))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DataV1 {
|
||||
pub message: Vec<u8>,
|
||||
pub reply_surbs: Vec<ReplySurb>,
|
||||
pub reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
}
|
||||
|
||||
impl Display for DataV1 {
|
||||
@@ -83,7 +85,7 @@ impl Display for DataV1 {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AdditionalSurbsV1 {
|
||||
pub reply_surbs: Vec<ReplySurb>,
|
||||
pub reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
}
|
||||
|
||||
impl Display for AdditionalSurbsV1 {
|
||||
@@ -98,7 +100,7 @@ impl Display for AdditionalSurbsV1 {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HeartbeatV1 {
|
||||
pub additional_reply_surbs: Vec<ReplySurb>,
|
||||
pub additional_reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
}
|
||||
|
||||
impl Display for HeartbeatV1 {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::requests::InvalidReplyRequestError;
|
||||
use crate::ReplySurb;
|
||||
use crate::{ReplySurb, ReplySurbWithKeyRotation};
|
||||
use nym_sphinx_params::SphinxKeyRotation;
|
||||
use nym_sphinx_types::constants::PAYLOAD_KEY_SEED_SIZE;
|
||||
use std::fmt::Display;
|
||||
use std::iter::once;
|
||||
@@ -13,21 +14,29 @@ const fn v2_reply_surb_serialised_len(num_hops: u8) -> usize {
|
||||
}
|
||||
|
||||
// sphinx doesn't support more than 5 hops (so cast to u8 is safe)
|
||||
// ASSUMPTION: all surbs are generated with the same parameters (if they're not, then the client is hurting itself)
|
||||
fn reply_surbs_hops(reply_surbs: &[ReplySurb]) -> u8 {
|
||||
// ASSUMPTION: all surbs are generated with the same parameters (if they're not, then the client is hurting itself),
|
||||
// which includes the same number of hops and the same underlying sphinx key rotation
|
||||
fn reply_surbs_hops(reply_surbs: &[ReplySurbWithKeyRotation]) -> u8 {
|
||||
reply_surbs
|
||||
.first()
|
||||
.map(|reply_surb| reply_surb.surb.materials_count() as u8)
|
||||
.map(|reply_surb| reply_surb.inner.surb.materials_count() as u8)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn v2_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize {
|
||||
fn key_rotation(reply_surbs: &[ReplySurbWithKeyRotation]) -> SphinxKeyRotation {
|
||||
reply_surbs
|
||||
.first()
|
||||
.map(|reply_surb| reply_surb.key_rotation)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn v2_reply_surbs_serialised_len(surbs: &[ReplySurbWithKeyRotation]) -> usize {
|
||||
let num_surbs = surbs.len();
|
||||
let num_hops = reply_surbs_hops(surbs);
|
||||
|
||||
// sanity checks; this should probably be removed later on
|
||||
if let Some(reply_surb) = surbs.first() {
|
||||
if !reply_surb.surb.uses_key_seeds() {
|
||||
if !reply_surb.inner.surb.uses_key_seeds() {
|
||||
error!("using v2 surbs encoding with legacy structure - the surbs will be unusable")
|
||||
}
|
||||
}
|
||||
@@ -35,14 +44,14 @@ fn v2_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize {
|
||||
// when serialising surbs are always prepended with:
|
||||
// - u16-encoded count,
|
||||
// - u8-encoded number of hops
|
||||
// - u8 reserved value
|
||||
// - u8-encoded sphinx key rotation (or unused for 'old' variant)
|
||||
4 + num_surbs * v2_reply_surb_serialised_len(num_hops)
|
||||
}
|
||||
|
||||
// NUM_SURBS (u16) || HOPS (u8) || RESERVED (u8) || SURB_DATA
|
||||
// NUM_SURBS (u16) || HOPS (u8) || KEY ROTATION (u8) || SURB_DATA
|
||||
fn recover_reply_surbs_v2(
|
||||
bytes: &[u8],
|
||||
) -> Result<(Vec<ReplySurb>, usize), InvalidReplyRequestError> {
|
||||
) -> Result<(Vec<ReplySurbWithKeyRotation>, usize), InvalidReplyRequestError> {
|
||||
if bytes.len() < 4 {
|
||||
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
|
||||
}
|
||||
@@ -50,7 +59,7 @@ fn recover_reply_surbs_v2(
|
||||
// we're not attaching more than 65k surbs...
|
||||
let num_surbs = u16::from_be_bytes([bytes[0], bytes[1]]);
|
||||
let num_hops = bytes[2];
|
||||
let _reserved = bytes[3];
|
||||
let key_rotation = SphinxKeyRotation::try_from(bytes[3])?;
|
||||
let mut consumed = 4;
|
||||
|
||||
let surb_size = v2_reply_surb_serialised_len(num_hops);
|
||||
@@ -61,7 +70,7 @@ fn recover_reply_surbs_v2(
|
||||
let mut reply_surbs = Vec::with_capacity(num_surbs as usize);
|
||||
for _ in 0..num_surbs as usize {
|
||||
let surb_bytes = &bytes[consumed..consumed + surb_size];
|
||||
let reply_surb = ReplySurb::from_bytes(surb_bytes)?;
|
||||
let reply_surb = ReplySurb::from_bytes(surb_bytes)?.with_key_rotation(key_rotation);
|
||||
reply_surbs.push(reply_surb);
|
||||
|
||||
consumed += surb_size;
|
||||
@@ -70,23 +79,25 @@ fn recover_reply_surbs_v2(
|
||||
Ok((reply_surbs, consumed))
|
||||
}
|
||||
|
||||
fn reply_surbs_bytes_v2(reply_surbs: &[ReplySurb]) -> impl Iterator<Item = u8> + use<'_> {
|
||||
fn reply_surbs_bytes_v2(
|
||||
reply_surbs: &[ReplySurbWithKeyRotation],
|
||||
) -> impl Iterator<Item = u8> + use<'_> {
|
||||
let num_surbs = reply_surbs.len() as u16;
|
||||
let num_hops = reply_surbs_hops(reply_surbs);
|
||||
let reserved = 0;
|
||||
let key_rotation = key_rotation(reply_surbs) as u8;
|
||||
|
||||
num_surbs
|
||||
.to_be_bytes()
|
||||
.into_iter()
|
||||
.chain(once(num_hops))
|
||||
.chain(once(reserved))
|
||||
.chain(reply_surbs.iter().flat_map(|surb| surb.to_bytes()))
|
||||
.chain(once(key_rotation))
|
||||
.chain(reply_surbs.iter().flat_map(|surb| surb.inner.to_bytes()))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DataV2 {
|
||||
pub message: Vec<u8>,
|
||||
pub reply_surbs: Vec<ReplySurb>,
|
||||
pub reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
}
|
||||
|
||||
impl Display for DataV2 {
|
||||
@@ -102,7 +113,7 @@ impl Display for DataV2 {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AdditionalSurbsV2 {
|
||||
pub reply_surbs: Vec<ReplySurb>,
|
||||
pub reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
}
|
||||
|
||||
impl Display for AdditionalSurbsV2 {
|
||||
@@ -117,7 +128,7 @@ impl Display for AdditionalSurbsV2 {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HeartbeatV2 {
|
||||
pub additional_reply_surbs: Vec<ReplySurb>,
|
||||
pub additional_reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
}
|
||||
|
||||
impl Display for HeartbeatV2 {
|
||||
|
||||
@@ -10,7 +10,9 @@ use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx_chunking::fragment::COVER_FRAG_ID;
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_params::packet_sizes::PacketSize;
|
||||
use nym_sphinx_params::{PacketEncryptionAlgorithm, PacketHkdfAlgorithm, PacketType};
|
||||
use nym_sphinx_params::{
|
||||
PacketEncryptionAlgorithm, PacketHkdfAlgorithm, PacketType, SphinxKeyRotation,
|
||||
};
|
||||
use nym_sphinx_types::NymPacket;
|
||||
use nym_topology::{NymRouteProvider, NymTopologyError};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
@@ -125,6 +127,9 @@ where
|
||||
let delays = nym_sphinx_routing::generate_hop_delays(average_packet_delay, route.len());
|
||||
let destination = full_address.as_sphinx_destination();
|
||||
|
||||
let rotation_id = topology.current_key_rotation();
|
||||
let sphinx_key_rotation = SphinxKeyRotation::from(rotation_id);
|
||||
|
||||
let first_hop_address =
|
||||
NymNodeRoutingAddress::try_from(route.first().unwrap().address).unwrap();
|
||||
|
||||
@@ -146,7 +151,12 @@ where
|
||||
)?,
|
||||
};
|
||||
|
||||
Ok(MixPacket::new(first_hop_address, packet, packet_type))
|
||||
Ok(MixPacket::new(
|
||||
first_hop_address,
|
||||
packet,
|
||||
packet_type,
|
||||
sphinx_key_rotation,
|
||||
))
|
||||
}
|
||||
|
||||
/// Helper function used to determine if given message represents a loop cover message.
|
||||
|
||||
@@ -11,5 +11,5 @@ repository = { workspace = true }
|
||||
nym-sphinx-addressing = { path = "../addressing" }
|
||||
nym-sphinx-params = { path = "../params" }
|
||||
nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] }
|
||||
nym-outfox = { path = "../../../nym-outfox" }
|
||||
nym-sphinx-anonymous-replies = { path = "../anonymous-replies" }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -2,24 +2,36 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError};
|
||||
use nym_sphinx_params::{PacketSize, PacketType};
|
||||
use nym_sphinx_params::{PacketSize, PacketType, SphinxKeyRotation};
|
||||
use nym_sphinx_types::{NymPacket, NymPacketError};
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use nym_sphinx_anonymous_replies::reply_surb::AppliedReplySurb;
|
||||
use nym_sphinx_params::key_rotation::InvalidSphinxKeyRotation;
|
||||
use nym_sphinx_params::packet_sizes::InvalidPacketSize;
|
||||
use nym_sphinx_params::packet_types::InvalidPacketType;
|
||||
use std::net::SocketAddr;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MixPacketFormattingError {
|
||||
#[error("too few bytes provided to recover from bytes")]
|
||||
TooFewBytesProvided,
|
||||
#[error("provided packet mode is invalid")]
|
||||
InvalidPacketType,
|
||||
#[error("received request had invalid size - received {0}")]
|
||||
InvalidPacketSize(usize),
|
||||
|
||||
#[error("provided packet mode is invalid: {0}")]
|
||||
InvalidPacketType(#[from] InvalidPacketType),
|
||||
|
||||
#[error("received request had an invalid packet size: {0}")]
|
||||
InvalidPacketSize(#[from] InvalidPacketSize),
|
||||
|
||||
#[error("provided key rotation is invalid: {0}")]
|
||||
InvalidKeyRotation(#[from] InvalidSphinxKeyRotation),
|
||||
|
||||
#[error("address field was incorrectly encoded")]
|
||||
InvalidAddress,
|
||||
|
||||
#[error("received sphinx packet was malformed")]
|
||||
MalformedSphinxPacket,
|
||||
|
||||
#[error("Packet: {0}")]
|
||||
Packet(#[from] NymPacketError),
|
||||
}
|
||||
@@ -30,20 +42,12 @@ impl From<NymNodeRoutingAddressError> for MixPacketFormattingError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MixPacket {
|
||||
next_hop: NymNodeRoutingAddress,
|
||||
packet: NymPacket,
|
||||
packet_type: PacketType,
|
||||
}
|
||||
|
||||
impl Debug for MixPacket {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"MixPacket to {:?} with packet_type {:?}. Packet {:?}",
|
||||
self.next_hop, self.packet_type, self.packet
|
||||
)
|
||||
}
|
||||
key_rotation: SphinxKeyRotation,
|
||||
}
|
||||
|
||||
impl MixPacket {
|
||||
@@ -51,11 +55,25 @@ impl MixPacket {
|
||||
next_hop: NymNodeRoutingAddress,
|
||||
packet: NymPacket,
|
||||
packet_type: PacketType,
|
||||
key_rotation: SphinxKeyRotation,
|
||||
) -> Self {
|
||||
MixPacket {
|
||||
next_hop,
|
||||
packet,
|
||||
packet_type,
|
||||
key_rotation,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_applied_surb(
|
||||
applied_reply_surb: AppliedReplySurb,
|
||||
packet_type: PacketType,
|
||||
) -> Self {
|
||||
MixPacket {
|
||||
next_hop: applied_reply_surb.first_hop_address(),
|
||||
key_rotation: applied_reply_surb.key_rotation(),
|
||||
packet: applied_reply_surb.into_packet(),
|
||||
packet_type,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +81,10 @@ impl MixPacket {
|
||||
self.next_hop
|
||||
}
|
||||
|
||||
pub fn next_hop_address(&self) -> SocketAddr {
|
||||
self.next_hop.into()
|
||||
}
|
||||
|
||||
pub fn packet(&self) -> &NymPacket {
|
||||
&self.packet
|
||||
}
|
||||
@@ -71,45 +93,94 @@ impl MixPacket {
|
||||
self.packet
|
||||
}
|
||||
|
||||
pub fn key_rotation(&self) -> SphinxKeyRotation {
|
||||
self.key_rotation
|
||||
}
|
||||
|
||||
pub fn packet_type(&self) -> PacketType {
|
||||
self.packet_type
|
||||
}
|
||||
|
||||
// the message is formatted as follows:
|
||||
// packet_type || FIRST_HOP || packet
|
||||
pub fn try_from_bytes(b: &[u8]) -> Result<Self, MixPacketFormattingError> {
|
||||
let packet_type = match PacketType::try_from(b[0]) {
|
||||
Ok(mode) => mode,
|
||||
Err(_) => return Err(MixPacketFormattingError::InvalidPacketType),
|
||||
};
|
||||
pub fn try_from_v1_bytes(b: &[u8]) -> Result<Self, MixPacketFormattingError> {
|
||||
// we need at least 1 byte to read packet type and another one to read type of the encoded first hop address
|
||||
if b.len() < 2 {
|
||||
return Err(MixPacketFormattingError::TooFewBytesProvided);
|
||||
}
|
||||
|
||||
let packet_type = PacketType::try_from(b[0])?;
|
||||
|
||||
let next_hop = NymNodeRoutingAddress::try_from_bytes(&b[1..])?;
|
||||
let addr_offset = next_hop.bytes_min_len();
|
||||
|
||||
let packet_data = &b[addr_offset + 1..];
|
||||
let packet_size = packet_data.len();
|
||||
if PacketSize::get_type(packet_size).is_err() {
|
||||
Err(MixPacketFormattingError::InvalidPacketSize(packet_size))
|
||||
} else {
|
||||
let packet = match packet_type {
|
||||
PacketType::Outfox => NymPacket::outfox_from_bytes(packet_data)?,
|
||||
_ => NymPacket::sphinx_from_bytes(packet_data)?,
|
||||
};
|
||||
|
||||
Ok(MixPacket {
|
||||
next_hop,
|
||||
packet,
|
||||
packet_type,
|
||||
})
|
||||
}
|
||||
// make sure the received data length corresponds to a valid packet
|
||||
let _ = PacketSize::get_type(packet_size)?;
|
||||
|
||||
let packet = match packet_type {
|
||||
PacketType::Mix => NymPacket::sphinx_from_bytes(packet_data)?,
|
||||
PacketType::Outfox => NymPacket::outfox_from_bytes(packet_data)?,
|
||||
};
|
||||
|
||||
Ok(MixPacket {
|
||||
next_hop,
|
||||
packet,
|
||||
packet_type,
|
||||
key_rotation: SphinxKeyRotation::Unknown,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Result<Vec<u8>, MixPacketFormattingError> {
|
||||
pub fn into_v1_bytes(self) -> Result<Vec<u8>, MixPacketFormattingError> {
|
||||
Ok(std::iter::once(self.packet_type as u8)
|
||||
.chain(self.next_hop.as_bytes())
|
||||
.chain(self.packet.to_bytes()?)
|
||||
.collect())
|
||||
}
|
||||
|
||||
// the message is formatted as follows:
|
||||
// packet_type || KEY_ROTATION || FIRST_HOP || packet
|
||||
pub fn try_from_v2_bytes(b: &[u8]) -> Result<Self, MixPacketFormattingError> {
|
||||
// we need at least 1 byte to read packet type, 1 byte to read key rotation
|
||||
// and finally another one to read type of the encoded first hop address
|
||||
if b.len() < 3 {
|
||||
return Err(MixPacketFormattingError::TooFewBytesProvided);
|
||||
}
|
||||
|
||||
let packet_type = PacketType::try_from(b[0])?;
|
||||
let key_rotation = SphinxKeyRotation::try_from(b[1])?;
|
||||
|
||||
let next_hop = NymNodeRoutingAddress::try_from_bytes(&b[2..])?;
|
||||
let addr_offset = next_hop.bytes_min_len();
|
||||
|
||||
let packet_data = &b[addr_offset + 2..];
|
||||
let packet_size = packet_data.len();
|
||||
|
||||
// make sure the received data length corresponds to a valid packet
|
||||
let _ = PacketSize::get_type(packet_size)?;
|
||||
|
||||
let packet = match packet_type {
|
||||
PacketType::Mix => NymPacket::sphinx_from_bytes(packet_data)?,
|
||||
PacketType::Outfox => NymPacket::outfox_from_bytes(packet_data)?,
|
||||
};
|
||||
|
||||
Ok(MixPacket {
|
||||
next_hop,
|
||||
packet,
|
||||
packet_type,
|
||||
key_rotation,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_v2_bytes(self) -> Result<Vec<u8>, MixPacketFormattingError> {
|
||||
Ok(std::iter::once(self.packet_type as u8)
|
||||
.chain(std::iter::once(self.key_rotation as u8))
|
||||
.chain(self.next_hop.as_bytes())
|
||||
.chain(self.packet.to_bytes()?)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: test for serialization and errors!
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use crate::packet::{FramedNymPacket, Header};
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use nym_sphinx_params::key_rotation::InvalidSphinxKeyRotation;
|
||||
use nym_sphinx_params::packet_sizes::{InvalidPacketSize, PacketSize};
|
||||
use nym_sphinx_params::packet_types::InvalidPacketType;
|
||||
use nym_sphinx_params::packet_version::{InvalidPacketVersion, PacketVersion};
|
||||
@@ -23,6 +24,9 @@ pub enum NymCodecError {
|
||||
#[error("the packet version information was malformed: {0}")]
|
||||
InvalidPacketVersion(#[from] InvalidPacketVersion),
|
||||
|
||||
#[error("the sphinx key rotation information was malformed: {0}")]
|
||||
InvalidSphinxKeyRotation(#[from] InvalidSphinxKeyRotation),
|
||||
|
||||
#[error("received unsupported packet version {received}. max supported is {max_supported}")]
|
||||
UnsupportedPacketVersion {
|
||||
received: PacketVersion,
|
||||
@@ -65,8 +69,8 @@ impl Decoder for NymCodec {
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
if src.is_empty() {
|
||||
// can't do anything if we have no bytes, but let's reserve enough for the most
|
||||
// conservative case, i.e. receiving an ack packet
|
||||
src.reserve(Header::SIZE + PacketSize::AckPacket.size());
|
||||
// conservative case, i.e. receiving a legacy ack packet
|
||||
src.reserve(Header::INITIAL_SIZE + PacketSize::AckPacket.size());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -77,17 +81,20 @@ impl Decoder for NymCodec {
|
||||
None => return Ok(None), // we have some data but not enough to get header back
|
||||
};
|
||||
|
||||
let header_size = header.encoded_size();
|
||||
let packet_size = header.packet_size.size();
|
||||
let frame_len = Header::SIZE + packet_size;
|
||||
|
||||
if src.len() < frame_len {
|
||||
let frame_size = header_size + packet_size;
|
||||
|
||||
if src.len() < frame_size {
|
||||
// we don't have enough bytes to read the rest of frame
|
||||
// (we have already read the full header)
|
||||
src.reserve(packet_size);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// advance buffer past the header - at this point we have enough bytes
|
||||
src.advance(Header::SIZE);
|
||||
src.advance(header_size);
|
||||
let packet_bytes = src.split_to(packet_size);
|
||||
let packet = if let Some(slice) = packet_bytes.get(..) {
|
||||
// here it could be debatable whether stream is corrupt or not,
|
||||
@@ -100,8 +107,7 @@ impl Decoder for NymCodec {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// let packet = SphinxPacket::from_bytes(&sphinx_packet_bytes)?;
|
||||
let nymsphinx_packet = FramedNymPacket { header, packet };
|
||||
let framed_packet = FramedNymPacket { header, packet };
|
||||
|
||||
// As per docs:
|
||||
// Before returning from the function, implementations should ensure that the buffer
|
||||
@@ -114,11 +120,11 @@ impl Decoder for NymCodec {
|
||||
// we also assume the next packet coming from the same client will use exactly the same versioning
|
||||
// as the current packet
|
||||
|
||||
let mut allocate_for_next_packet = Header::SIZE + PacketSize::AckPacket.size();
|
||||
let mut allocate_for_next_packet = header.encoded_size() + PacketSize::AckPacket.size();
|
||||
if !src.is_empty() {
|
||||
match Header::decode(src) {
|
||||
Ok(Some(next_header)) => {
|
||||
allocate_for_next_packet = Header::SIZE + next_header.packet_size.size();
|
||||
allocate_for_next_packet = next_header.frame_size();
|
||||
}
|
||||
Ok(None) => {
|
||||
// we don't have enough information to know how much to reserve, fallback to the ack case
|
||||
@@ -126,22 +132,52 @@ impl Decoder for NymCodec {
|
||||
|
||||
// the next frame will be malformed but let's leave handling the error to the next
|
||||
// call to 'decode', as presumably, the current sphinx packet is still valid
|
||||
Err(_) => return Ok(Some(nymsphinx_packet)),
|
||||
Err(_) => return Ok(Some(framed_packet)),
|
||||
};
|
||||
}
|
||||
src.reserve(allocate_for_next_packet);
|
||||
Ok(Some(nymsphinx_packet))
|
||||
Ok(Some(framed_packet))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod packet_encoding {
|
||||
use super::*;
|
||||
use nym_sphinx_params::packet_version::{
|
||||
CURRENT_PACKET_VERSION, INITIAL_PACKET_VERSION_NUMBER,
|
||||
};
|
||||
use nym_sphinx_params::PacketType;
|
||||
use nym_sphinx_types::{
|
||||
Delay as SphinxDelay, Destination, DestinationAddressBytes, Node, NodeAddressBytes,
|
||||
PrivateKey, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH,
|
||||
NymPacket, PrivateKey, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH,
|
||||
};
|
||||
|
||||
fn dummy_header() -> Header {
|
||||
Header {
|
||||
packet_version: CURRENT_PACKET_VERSION,
|
||||
packet_size: Default::default(),
|
||||
key_rotation: Default::default(),
|
||||
packet_type: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn dummy_outfox() -> Header {
|
||||
Header {
|
||||
packet_type: PacketType::Outfox,
|
||||
packet_size: PacketSize::OutfoxRegularPacket,
|
||||
..dummy_legacy_header()
|
||||
}
|
||||
}
|
||||
|
||||
fn dummy_legacy_header() -> Header {
|
||||
Header {
|
||||
packet_version: PacketVersion::try_from(INITIAL_PACKET_VERSION_NUMBER).unwrap(),
|
||||
packet_size: Default::default(),
|
||||
key_rotation: Default::default(),
|
||||
packet_type: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn random_pubkey() -> nym_sphinx_types::PublicKey {
|
||||
let private_key = PrivateKey::random();
|
||||
(&private_key).into()
|
||||
@@ -222,7 +258,7 @@ mod packet_encoding {
|
||||
|
||||
#[test]
|
||||
fn whole_packet_can_be_decoded_from_a_valid_encoded_instance() {
|
||||
let header = Default::default();
|
||||
let header = dummy_header();
|
||||
let sphinx_packet = make_valid_sphinx_packet(Default::default());
|
||||
let sphinx_bytes = sphinx_packet.to_bytes().unwrap();
|
||||
|
||||
@@ -241,7 +277,7 @@ mod packet_encoding {
|
||||
|
||||
#[test]
|
||||
fn whole_outfox_can_be_decoded_from_a_valid_encoded_instance() {
|
||||
let header = Header::outfox();
|
||||
let header = dummy_outfox();
|
||||
let packet = make_valid_outfox_packet(PacketSize::OutfoxRegularPacket);
|
||||
let packet_bytes = packet.to_bytes().unwrap();
|
||||
|
||||
@@ -269,7 +305,7 @@ mod packet_encoding {
|
||||
assert!(NymCodec.decode(&mut empty_bytes).unwrap().is_none());
|
||||
assert_eq!(
|
||||
empty_bytes.capacity(),
|
||||
Header::SIZE + PacketSize::AckPacket.size()
|
||||
Header::INITIAL_SIZE + PacketSize::AckPacket.size()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -287,13 +323,14 @@ mod packet_encoding {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::new(),
|
||||
packet_size,
|
||||
..Default::default()
|
||||
key_rotation: Default::default(),
|
||||
packet_type: Default::default(),
|
||||
};
|
||||
let mut bytes = BytesMut::new();
|
||||
header.encode(&mut bytes);
|
||||
assert!(NymCodec.decode(&mut bytes).unwrap().is_none());
|
||||
|
||||
assert_eq!(bytes.capacity(), Header::SIZE + packet_size.size())
|
||||
assert_eq!(bytes.capacity(), Header::V8_SIZE + packet_size.size())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +338,7 @@ mod packet_encoding {
|
||||
fn for_full_frame_with_versioned_header() {
|
||||
// if full frame is used exactly, there should be enough space for header + ack packet
|
||||
let packet = FramedNymPacket {
|
||||
header: Header::default(),
|
||||
header: dummy_header(),
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
@@ -310,7 +347,7 @@ mod packet_encoding {
|
||||
assert!(NymCodec.decode(&mut bytes).unwrap().is_some());
|
||||
assert_eq!(
|
||||
bytes.capacity(),
|
||||
Header::SIZE + PacketSize::AckPacket.size()
|
||||
Header::V8_SIZE + PacketSize::AckPacket.size()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -327,7 +364,7 @@ mod packet_encoding {
|
||||
|
||||
for packet_size in packet_sizes {
|
||||
let first_packet = FramedNymPacket {
|
||||
header: Header::default(),
|
||||
header: dummy_header(),
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
@@ -346,12 +383,12 @@ mod packet_encoding {
|
||||
#[test]
|
||||
fn can_decode_two_packets_immediately() {
|
||||
let packet1 = FramedNymPacket {
|
||||
header: Header::default(),
|
||||
header: dummy_header(),
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
let packet2 = FramedNymPacket {
|
||||
header: Header::default(),
|
||||
header: dummy_header(),
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
@@ -368,12 +405,12 @@ mod packet_encoding {
|
||||
#[test]
|
||||
fn can_decode_two_packets_in_separate_calls() {
|
||||
let packet1 = FramedNymPacket {
|
||||
header: Header::default(),
|
||||
header: dummy_header(),
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
let packet2 = FramedNymPacket {
|
||||
header: Header::default(),
|
||||
header: dummy_header(),
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
use crate::codec::NymCodecError;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_params::key_rotation::SphinxKeyRotation;
|
||||
use nym_sphinx_params::packet_sizes::PacketSize;
|
||||
use nym_sphinx_params::packet_version::{PacketVersion, CURRENT_PACKET_VERSION};
|
||||
use nym_sphinx_params::PacketType;
|
||||
@@ -17,8 +19,20 @@ pub struct FramedNymPacket {
|
||||
pub(crate) packet: NymPacket,
|
||||
}
|
||||
|
||||
impl From<MixPacket> for FramedNymPacket {
|
||||
fn from(packet: MixPacket) -> Self {
|
||||
let typ = packet.packet_type();
|
||||
let rot = packet.key_rotation();
|
||||
FramedNymPacket::new(packet.into_packet(), typ, rot)
|
||||
}
|
||||
}
|
||||
|
||||
impl FramedNymPacket {
|
||||
pub fn new(packet: NymPacket, packet_type: PacketType) -> Self {
|
||||
pub fn new(
|
||||
packet: NymPacket,
|
||||
packet_type: PacketType,
|
||||
key_rotation: SphinxKeyRotation,
|
||||
) -> Self {
|
||||
// If this fails somebody is using the library in a super incorrect way, because they
|
||||
// already managed to somehow create a sphinx packet
|
||||
let packet_size = PacketSize::get_type(packet.len()).unwrap();
|
||||
@@ -26,6 +40,7 @@ impl FramedNymPacket {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::new(),
|
||||
packet_size,
|
||||
key_rotation,
|
||||
packet_type,
|
||||
};
|
||||
|
||||
@@ -52,6 +67,10 @@ impl FramedNymPacket {
|
||||
&self.packet
|
||||
}
|
||||
|
||||
pub fn key_rotation(&self) -> SphinxKeyRotation {
|
||||
self.header.key_rotation
|
||||
}
|
||||
|
||||
pub fn is_sphinx(&self) -> bool {
|
||||
self.packet.is_sphinx()
|
||||
}
|
||||
@@ -60,13 +79,16 @@ impl FramedNymPacket {
|
||||
// Contains any metadata that might be useful for sending between mix nodes.
|
||||
// TODO: in theory all those data could be put in a single `u8` by setting appropriate bits,
|
||||
// but would that really be worth it?
|
||||
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub struct Header {
|
||||
/// Represents the wire format version used to construct this packet.
|
||||
pub(crate) packet_version: PacketVersion,
|
||||
pub packet_version: PacketVersion,
|
||||
|
||||
/// Represents type and consequently size of the included SphinxPacket.
|
||||
pub(crate) packet_size: PacketSize,
|
||||
pub packet_size: PacketSize,
|
||||
|
||||
/// Represents information regarding which key rotation has been used for constructing this packet.
|
||||
pub key_rotation: SphinxKeyRotation,
|
||||
|
||||
/// Represents whether this packet is sent in a `vpn_mode` meaning it should not get delayed
|
||||
/// and shared keys might get reused. Mixnodes are capable of inferring this mode from the
|
||||
@@ -77,35 +99,48 @@ pub struct Header {
|
||||
/// (note: this will be behind some encryption, either something implemented by us or some SSL action)
|
||||
// Note: currently packet_type is deprecated but is still left as a concept behind to not break
|
||||
// compatibility with existing network
|
||||
pub(crate) packet_type: PacketType,
|
||||
pub packet_type: PacketType,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub(crate) const SIZE: usize = 3;
|
||||
|
||||
pub fn outfox() -> Header {
|
||||
Header {
|
||||
packet_version: PacketVersion::default(),
|
||||
packet_size: PacketSize::OutfoxRegularPacket,
|
||||
packet_type: PacketType::Outfox,
|
||||
}
|
||||
}
|
||||
pub(crate) const INITIAL_SIZE: usize = 3;
|
||||
pub(crate) const V8_SIZE: usize = 4;
|
||||
|
||||
pub(crate) fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.reserve(Self::SIZE);
|
||||
let len = self.encoded_size();
|
||||
|
||||
if dst.len() < len {
|
||||
dst.reserve(len);
|
||||
}
|
||||
|
||||
dst.put_u8(self.packet_version.as_u8());
|
||||
dst.put_u8(self.packet_size as u8);
|
||||
dst.put_u8(self.packet_type as u8);
|
||||
|
||||
if !self.packet_version.is_initial() {
|
||||
dst.put_u8(self.key_rotation as u8)
|
||||
}
|
||||
|
||||
// reserve bytes for the actual packet
|
||||
dst.reserve(self.packet_size.size());
|
||||
}
|
||||
|
||||
pub(crate) fn frame_size(&self) -> usize {
|
||||
self.encoded_size() + self.packet_size.size()
|
||||
}
|
||||
|
||||
pub(crate) fn encoded_size(&self) -> usize {
|
||||
if self.packet_version.is_initial() {
|
||||
Self::INITIAL_SIZE
|
||||
} else {
|
||||
Self::V8_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decode(src: &mut BytesMut) -> Result<Option<Self>, NymCodecError> {
|
||||
if src.len() < Self::SIZE {
|
||||
if src.len() < Self::INITIAL_SIZE {
|
||||
// can't do anything if we don't have enough bytes - but reserve enough for the next call
|
||||
src.reserve(Self::SIZE);
|
||||
src.reserve(Self::INITIAL_SIZE);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -119,10 +154,23 @@ impl Header {
|
||||
});
|
||||
}
|
||||
|
||||
// we need to be able to decode the full header
|
||||
if !packet_version.is_initial() && src.len() < Self::V8_SIZE {
|
||||
src.reserve(1);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let key_rotation = if packet_version.is_initial() {
|
||||
SphinxKeyRotation::Unknown
|
||||
} else {
|
||||
SphinxKeyRotation::try_from(src[3])?
|
||||
};
|
||||
|
||||
Ok(Some(Header {
|
||||
packet_version,
|
||||
packet_size: PacketSize::try_from(src[1])?,
|
||||
packet_type: PacketType::try_from(src[2])?,
|
||||
key_rotation,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -130,10 +178,20 @@ impl Header {
|
||||
#[cfg(test)]
|
||||
mod header_encoding {
|
||||
use super::*;
|
||||
use nym_sphinx_params::packet_version::INITIAL_PACKET_VERSION_NUMBER;
|
||||
|
||||
fn dummy_header() -> Header {
|
||||
Header {
|
||||
packet_version: CURRENT_PACKET_VERSION,
|
||||
packet_size: Default::default(),
|
||||
key_rotation: Default::default(),
|
||||
packet_type: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_can_be_decoded_from_a_valid_encoded_instance() {
|
||||
let header = Header::default();
|
||||
let header = dummy_header();
|
||||
let mut bytes = BytesMut::new();
|
||||
header.encode(&mut bytes);
|
||||
let decoded = Header::decode(&mut bytes).unwrap().unwrap();
|
||||
@@ -153,6 +211,7 @@ mod header_encoding {
|
||||
PacketVersion::new().as_u8(),
|
||||
unknown_packet_size,
|
||||
PacketType::default() as u8,
|
||||
SphinxKeyRotation::EvenRotation as u8,
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
@@ -167,7 +226,9 @@ mod header_encoding {
|
||||
|
||||
let mut bytes = BytesMut::from(
|
||||
[
|
||||
PacketVersion::new().as_u8(),
|
||||
PacketVersion::try_from(INITIAL_PACKET_VERSION_NUMBER)
|
||||
.unwrap()
|
||||
.as_u8(),
|
||||
PacketSize::default() as u8,
|
||||
unknown_packet_type,
|
||||
]
|
||||
@@ -181,12 +242,12 @@ mod header_encoding {
|
||||
let mut empty_bytes = BytesMut::new();
|
||||
let decode_attempt_1 = Header::decode(&mut empty_bytes).unwrap();
|
||||
assert!(decode_attempt_1.is_none());
|
||||
assert!(empty_bytes.capacity() > Header::SIZE);
|
||||
assert!(empty_bytes.capacity() > Header::V8_SIZE);
|
||||
|
||||
let mut empty_bytes = BytesMut::with_capacity(1);
|
||||
let decode_attempt_2 = Header::decode(&mut empty_bytes).unwrap();
|
||||
assert!(decode_attempt_2.is_none());
|
||||
assert!(empty_bytes.capacity() > Header::SIZE);
|
||||
assert!(empty_bytes.capacity() > Header::V8_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -202,7 +263,7 @@ mod header_encoding {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::new(),
|
||||
packet_size,
|
||||
..Default::default()
|
||||
..dummy_header()
|
||||
};
|
||||
let mut bytes = BytesMut::new();
|
||||
header.encode(&mut bytes);
|
||||
@@ -217,6 +278,7 @@ mod header_encoding {
|
||||
let unchecked_header = Header {
|
||||
packet_version: future_version,
|
||||
packet_size: PacketSize::RegularPacket,
|
||||
key_rotation: SphinxKeyRotation::EvenRotation,
|
||||
packet_type: PacketType::Mix,
|
||||
};
|
||||
let mut bytes = BytesMut::new();
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::packet::FramedNymPacket;
|
||||
use nym_sphinx_acknowledgements::surb_ack::{SurbAck, SurbAckRecoveryError};
|
||||
use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError};
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_params::{PacketSize, PacketType};
|
||||
use nym_sphinx_params::{PacketSize, PacketType, SphinxKeyRotation};
|
||||
use nym_sphinx_types::header::shared_secret::ExpandedSharedSecret;
|
||||
use nym_sphinx_types::{
|
||||
Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError,
|
||||
@@ -103,10 +103,18 @@ pub enum PacketProcessingError {
|
||||
#[error("attempted to partially process an outfox packet")]
|
||||
PartialOutfoxProcessing,
|
||||
|
||||
#[error("the key needed for unwrapping this packet has already expired")]
|
||||
ExpiredKey,
|
||||
|
||||
#[error("this packet has already been processed before")]
|
||||
PacketReplay,
|
||||
}
|
||||
|
||||
pub struct PartialyUnwrappedPacketWithKeyRotation {
|
||||
pub packet: PartiallyUnwrappedPacket,
|
||||
pub used_key_rotation: u32,
|
||||
}
|
||||
|
||||
pub struct PartiallyUnwrappedPacket {
|
||||
received_data: FramedNymPacket,
|
||||
partial_result: PartialMixProcessingResult,
|
||||
@@ -119,16 +127,19 @@ impl PartiallyUnwrappedPacket {
|
||||
pub fn new(
|
||||
received_data: FramedNymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<Self, PacketProcessingError> {
|
||||
) -> Result<Self, (FramedNymPacket, PacketProcessingError)> {
|
||||
let partial_result = match received_data.packet() {
|
||||
NymPacket::Sphinx(packet) => {
|
||||
let expanded_shared_secret =
|
||||
packet.header.compute_expanded_shared_secret(sphinx_key);
|
||||
|
||||
// don't continue if the header is malformed
|
||||
packet
|
||||
if let Err(err) = packet
|
||||
.header
|
||||
.ensure_header_integrity(&expanded_shared_secret)?;
|
||||
.ensure_header_integrity(&expanded_shared_secret)
|
||||
{
|
||||
return Err((received_data, err.into()));
|
||||
}
|
||||
|
||||
PartialMixProcessingResult::Sphinx {
|
||||
expanded_shared_secret,
|
||||
@@ -147,6 +158,7 @@ impl PartiallyUnwrappedPacket {
|
||||
let packet_size = self.received_data.packet_size();
|
||||
let packet_type = self.received_data.packet_type();
|
||||
|
||||
let key_rotation = self.received_data.header.key_rotation;
|
||||
let packet = self.received_data.into_inner();
|
||||
|
||||
// currently partial unwrapping is only implemented for sphinx packets.
|
||||
@@ -161,12 +173,22 @@ impl PartiallyUnwrappedPacket {
|
||||
return Err(PacketProcessingError::PartialOutfoxProcessing);
|
||||
};
|
||||
let processed_packet = packet.process_with_expanded_secret(&expanded_shared_secret)?;
|
||||
wrap_processed_sphinx_packet(processed_packet, packet_size, packet_type)
|
||||
wrap_processed_sphinx_packet(processed_packet, packet_size, packet_type, key_rotation)
|
||||
}
|
||||
|
||||
pub fn replay_tag(&self) -> Option<&[u8; REPLAY_TAG_SIZE]> {
|
||||
self.partial_result.replay_tag()
|
||||
}
|
||||
|
||||
pub fn with_key_rotation(
|
||||
self,
|
||||
used_key_rotation: u32,
|
||||
) -> PartialyUnwrappedPacketWithKeyRotation {
|
||||
PartialyUnwrappedPacketWithKeyRotation {
|
||||
packet: self,
|
||||
used_key_rotation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(FramedNymPacket, PartialMixProcessingResult)> for PartiallyUnwrappedPacket {
|
||||
@@ -186,13 +208,14 @@ pub fn process_framed_packet(
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
let packet_size = received.packet_size();
|
||||
let packet_type = received.packet_type();
|
||||
let key_rotation = received.key_rotation();
|
||||
|
||||
// unwrap the sphinx packet
|
||||
let processed_packet = perform_framed_unwrapping(received, sphinx_key)?;
|
||||
|
||||
// for forward packets, extract next hop and set delay (but do NOT delay here)
|
||||
// for final packets, extract SURBAck
|
||||
perform_final_processing(processed_packet, packet_size, packet_type)
|
||||
perform_final_processing(processed_packet, packet_size, packet_type, key_rotation)
|
||||
}
|
||||
|
||||
fn perform_framed_unwrapping(
|
||||
@@ -217,6 +240,7 @@ fn wrap_processed_sphinx_packet(
|
||||
packet: nym_sphinx_types::ProcessedPacket,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
key_rotation: SphinxKeyRotation,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
let processing_data = match packet.data {
|
||||
ProcessedPacketData::ForwardHop {
|
||||
@@ -228,6 +252,7 @@ fn wrap_processed_sphinx_packet(
|
||||
next_hop_address,
|
||||
delay,
|
||||
packet_type,
|
||||
key_rotation,
|
||||
),
|
||||
// right now there's no use for the surb_id included in the header - probably it should get removed from the
|
||||
// sphinx all together?
|
||||
@@ -240,6 +265,7 @@ fn wrap_processed_sphinx_packet(
|
||||
payload.recover_plaintext()?,
|
||||
packet_size,
|
||||
packet_type,
|
||||
key_rotation,
|
||||
),
|
||||
}?;
|
||||
|
||||
@@ -253,6 +279,7 @@ fn wrap_processed_outfox_packet(
|
||||
packet: OutfoxProcessedPacket,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
key_rotation: SphinxKeyRotation,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
let next_address = *packet.next_address();
|
||||
let packet = packet.into_packet();
|
||||
@@ -262,6 +289,7 @@ fn wrap_processed_outfox_packet(
|
||||
packet.recover_plaintext()?.to_vec(),
|
||||
packet_size,
|
||||
packet_type,
|
||||
key_rotation,
|
||||
)?;
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
@@ -272,6 +300,7 @@ fn wrap_processed_outfox_packet(
|
||||
NymNodeRoutingAddress::try_from_bytes(&next_address)?,
|
||||
NymPacket::Outfox(packet),
|
||||
PacketType::Outfox,
|
||||
SphinxKeyRotation::Unknown,
|
||||
);
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
@@ -287,13 +316,14 @@ fn perform_final_processing(
|
||||
packet: NymProcessedPacket,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
key_rotation: SphinxKeyRotation,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
match packet {
|
||||
NymProcessedPacket::Sphinx(packet) => {
|
||||
wrap_processed_sphinx_packet(packet, packet_size, packet_type)
|
||||
wrap_processed_sphinx_packet(packet, packet_size, packet_type, key_rotation)
|
||||
}
|
||||
NymProcessedPacket::Outfox(packet) => {
|
||||
wrap_processed_outfox_packet(packet, packet_size, packet_type)
|
||||
wrap_processed_outfox_packet(packet, packet_size, packet_type, key_rotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,8 +333,10 @@ fn process_final_hop(
|
||||
payload: Vec<u8>,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
key_rotation: SphinxKeyRotation,
|
||||
) -> Result<MixProcessingResultData, PacketProcessingError> {
|
||||
let (forward_ack, message) = split_into_ack_and_message(payload, packet_size, packet_type)?;
|
||||
let (forward_ack, message) =
|
||||
split_into_ack_and_message(payload, packet_size, packet_type, key_rotation)?;
|
||||
|
||||
Ok(MixProcessingResultData::FinalHop {
|
||||
final_hop_data: ProcessedFinalHop {
|
||||
@@ -319,6 +351,7 @@ fn split_into_ack_and_message(
|
||||
data: Vec<u8>,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
key_rotation: SphinxKeyRotation,
|
||||
) -> Result<(Option<MixPacket>, Vec<u8>), PacketProcessingError> {
|
||||
match packet_size {
|
||||
PacketSize::AckPacket | PacketSize::OutfoxAckPacket => {
|
||||
@@ -340,7 +373,7 @@ fn split_into_ack_and_message(
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type);
|
||||
let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type, key_rotation);
|
||||
Ok((Some(forward_ack), message))
|
||||
}
|
||||
}
|
||||
@@ -368,10 +401,11 @@ fn process_forward_hop(
|
||||
forward_address: NodeAddressBytes,
|
||||
delay: SphinxDelay,
|
||||
packet_type: PacketType,
|
||||
key_rotation: SphinxKeyRotation,
|
||||
) -> Result<MixProcessingResultData, PacketProcessingError> {
|
||||
let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?;
|
||||
|
||||
let packet = MixPacket::new(next_hop_address, packet, packet_type);
|
||||
let packet = MixPacket::new(next_hop_address, packet, packet_type, key_rotation);
|
||||
Ok(MixProcessingResultData::ForwardHop {
|
||||
packet,
|
||||
delay: Some(delay),
|
||||
@@ -422,9 +456,13 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn splitting_into_ack_and_message_returns_whole_data_for_ack() {
|
||||
let data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) + 10];
|
||||
let (ack, message) =
|
||||
split_into_ack_and_message(data.clone(), PacketSize::AckPacket, PacketType::Mix)
|
||||
.unwrap();
|
||||
let (ack, message) = split_into_ack_and_message(
|
||||
data.clone(),
|
||||
PacketSize::AckPacket,
|
||||
PacketType::Mix,
|
||||
SphinxKeyRotation::EvenRotation,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(ack.is_none());
|
||||
assert_eq!(data, message)
|
||||
}
|
||||
@@ -436,6 +474,7 @@ mod tests {
|
||||
data.clone(),
|
||||
PacketSize::OutfoxAckPacket,
|
||||
PacketType::Outfox,
|
||||
SphinxKeyRotation::EvenRotation,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(ack.is_none());
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum SphinxKeyRotation {
|
||||
// for legacy packets, where there's no explicit information which key has been used
|
||||
#[default]
|
||||
Unknown = 0,
|
||||
|
||||
OddRotation = 1,
|
||||
|
||||
EvenRotation = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{received} is not a valid encoding of a sphinx key rotation")]
|
||||
pub struct InvalidSphinxKeyRotation {
|
||||
received: u8,
|
||||
}
|
||||
|
||||
// convert from particular rotation id into SphinxKeyRotation variant
|
||||
impl From<u32> for SphinxKeyRotation {
|
||||
fn from(value: u32) -> Self {
|
||||
if value == 0 || value == u32::MAX {
|
||||
SphinxKeyRotation::Unknown
|
||||
} else if value % 2 == 0 {
|
||||
SphinxKeyRotation::EvenRotation
|
||||
} else {
|
||||
SphinxKeyRotation::OddRotation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// convert from an encoded SphinxKeyRotation into particular variant
|
||||
// if value is actually provided, it MUST be one of the two. otherwise is invalid
|
||||
impl TryFrom<u8> for SphinxKeyRotation {
|
||||
type Error = InvalidSphinxKeyRotation;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
_ if value == (Self::Unknown as u8) => Ok(Self::Unknown),
|
||||
_ if value == (Self::OddRotation as u8) => Ok(Self::OddRotation),
|
||||
_ if value == (Self::EvenRotation as u8) => Ok(Self::EvenRotation),
|
||||
received => Err(InvalidSphinxKeyRotation { received }),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,12 @@ use nym_crypto::Aes256GcmSiv;
|
||||
type Aes128Ctr = ctr::Ctr64BE<Aes128>;
|
||||
|
||||
// Re-export for ease of use
|
||||
pub use key_rotation::SphinxKeyRotation;
|
||||
pub use packet_sizes::PacketSize;
|
||||
pub use packet_types::PacketType;
|
||||
pub use packet_version::PacketVersion;
|
||||
|
||||
pub mod key_rotation;
|
||||
pub mod packet_sizes;
|
||||
pub mod packet_types;
|
||||
pub mod packet_version;
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::fmt;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("{received} is not a valid packet mode tag")]
|
||||
#[error("{received} is not a valid packet type tag")]
|
||||
pub struct InvalidPacketType {
|
||||
received: u8,
|
||||
}
|
||||
|
||||
@@ -10,13 +10,15 @@ use thiserror::Error;
|
||||
// - packet_version (starting with v1.1.0)
|
||||
// - packet_size indicator
|
||||
// - packet_type
|
||||
// - sphinx key rotation (starting with v1.12.0/v1.13.0 - either Cheddar or Dolcelatte release)
|
||||
|
||||
// it also just so happens that the only valid values for packet_size indicator include values 1-6
|
||||
// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet,
|
||||
// otherwise we should treat it as legacy
|
||||
/// Increment it whenever we perform any breaking change in the wire format!
|
||||
pub const INITIAL_PACKET_VERSION_NUMBER: u8 = 7;
|
||||
|
||||
pub const CURRENT_PACKET_VERSION_NUMBER: u8 = INITIAL_PACKET_VERSION_NUMBER;
|
||||
pub const KEY_ROTATION_VERSION_NUMBER: u8 = 8;
|
||||
pub const CURRENT_PACKET_VERSION_NUMBER: u8 = KEY_ROTATION_VERSION_NUMBER;
|
||||
pub const CURRENT_PACKET_VERSION: PacketVersion =
|
||||
PacketVersion::unchecked(CURRENT_PACKET_VERSION_NUMBER);
|
||||
|
||||
@@ -38,6 +40,10 @@ impl PacketVersion {
|
||||
PacketVersion(CURRENT_PACKET_VERSION_NUMBER)
|
||||
}
|
||||
|
||||
pub fn is_initial(&self) -> bool {
|
||||
self.0 == INITIAL_PACKET_VERSION_NUMBER
|
||||
}
|
||||
|
||||
const fn unchecked(version: u8) -> PacketVersion {
|
||||
PacketVersion(version)
|
||||
}
|
||||
|
||||
@@ -13,13 +13,14 @@ use nym_sphinx_anonymous_replies::reply_surb::ReplySurb;
|
||||
use nym_sphinx_chunking::fragment::{Fragment, FragmentIdentifier};
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_params::packet_sizes::PacketSize;
|
||||
use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm};
|
||||
use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, SphinxKeyRotation};
|
||||
use nym_sphinx_types::{Delay, NymPacket};
|
||||
use nym_topology::{NymRouteProvider, NymTopologyError};
|
||||
use rand::{CryptoRng, Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use tracing::*;
|
||||
|
||||
use nym_sphinx_anonymous_replies::ReplySurbWithKeyRotation;
|
||||
use nym_sphinx_chunking::monitoring;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -103,7 +104,7 @@ pub trait FragmentPreparer {
|
||||
fragment: Fragment,
|
||||
topology: &NymRouteProvider,
|
||||
ack_key: &AckKey,
|
||||
reply_surb: ReplySurb,
|
||||
reply_surb: ReplySurbWithKeyRotation,
|
||||
packet_sender: &Recipient,
|
||||
packet_type: PacketType,
|
||||
) -> Result<PreparedFragment, NymTopologyError> {
|
||||
@@ -148,7 +149,7 @@ pub trait FragmentPreparer {
|
||||
|
||||
// the unwrap here is fine as the failures can only originate from attempting to use invalid payload lengths
|
||||
// and we just very carefully constructed a (presumably) valid one
|
||||
let (sphinx_packet, first_hop_address) = reply_surb
|
||||
let applied_surb = reply_surb
|
||||
.apply_surb(packet_payload, packet_size, packet_type)
|
||||
.unwrap();
|
||||
|
||||
@@ -157,7 +158,7 @@ pub trait FragmentPreparer {
|
||||
// well as the total delay of the ack packet.
|
||||
// we don't know the delays inside the reply surbs so we use best-effort estimation from our poisson distribution
|
||||
total_delay: expected_forward_delay + ack_delay,
|
||||
mix_packet: MixPacket::new(first_hop_address, sphinx_packet, packet_type),
|
||||
mix_packet: MixPacket::from_applied_surb(applied_surb, packet_type),
|
||||
fragment_identifier,
|
||||
})
|
||||
}
|
||||
@@ -211,6 +212,9 @@ pub trait FragmentPreparer {
|
||||
let packet_size = PacketSize::get_type_from_plaintext(expected_plaintext, packet_type)
|
||||
.expect("the message has been incorrectly fragmented");
|
||||
|
||||
let rotation_id = topology.current_key_rotation();
|
||||
let sphinx_key_rotation = SphinxKeyRotation::from(rotation_id);
|
||||
|
||||
let fragment_identifier = fragment.fragment_identifier();
|
||||
|
||||
// create an ack
|
||||
@@ -279,7 +283,7 @@ pub trait FragmentPreparer {
|
||||
// well as the total delay of the ack packet.
|
||||
// note that the last hop of the packet is a gateway that does not do any delays
|
||||
total_delay: delays.iter().take(delays.len() - 1).sum::<Delay>() + ack_delay,
|
||||
mix_packet: MixPacket::new(first_hop_address, packet, packet_type),
|
||||
mix_packet: MixPacket::new(first_hop_address, packet, packet_type, sphinx_key_rotation),
|
||||
fragment_identifier,
|
||||
})
|
||||
}
|
||||
@@ -371,10 +375,12 @@ where
|
||||
use_legacy_reply_surb_format: bool,
|
||||
amount: usize,
|
||||
topology: &NymRouteProvider,
|
||||
) -> Result<Vec<ReplySurb>, NymTopologyError> {
|
||||
) -> Result<Vec<ReplySurbWithKeyRotation>, NymTopologyError> {
|
||||
let mut reply_surbs = Vec::with_capacity(amount);
|
||||
let disabled_mix_hops = self.mix_hops_disabled();
|
||||
|
||||
let key_rotation = SphinxKeyRotation::from(topology.current_key_rotation());
|
||||
|
||||
for _ in 0..amount {
|
||||
let reply_surb = ReplySurb::construct(
|
||||
&mut self.rng,
|
||||
@@ -383,7 +389,8 @@ where
|
||||
use_legacy_reply_surb_format,
|
||||
topology,
|
||||
disabled_mix_hops, // TODO: support SURBs with no mix hops after changes to surb format / construction
|
||||
)?;
|
||||
)?
|
||||
.with_key_rotation(key_rotation);
|
||||
reply_surbs.push(reply_surb)
|
||||
}
|
||||
|
||||
@@ -395,7 +402,7 @@ where
|
||||
fragment: Fragment,
|
||||
topology: &NymRouteProvider,
|
||||
ack_key: &AckKey,
|
||||
reply_surb: ReplySurb,
|
||||
reply_surb: ReplySurbWithKeyRotation,
|
||||
packet_type: PacketType,
|
||||
) -> Result<PreparedFragment, NymTopologyError> {
|
||||
let sender = self.sender_address;
|
||||
|
||||
@@ -50,6 +50,7 @@ pub enum NymPacketError {
|
||||
FromSlice(#[from] TryFromSliceError),
|
||||
}
|
||||
|
||||
// TODO: wrap that guy and add extra metadata to indicate key rotation?
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum NymPacket {
|
||||
#[cfg(feature = "sphinx")]
|
||||
|
||||
@@ -9,4 +9,5 @@ repository = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
pem = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
+41
-12
@@ -5,12 +5,35 @@ use crate::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use pem::Pem;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::debug;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
pub mod traits;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ZeroizingPem(Pem);
|
||||
|
||||
impl Zeroize for ZeroizingPem {
|
||||
fn zeroize(&mut self) {
|
||||
self.0.tag.zeroize();
|
||||
self.0.contents.zeroize();
|
||||
}
|
||||
}
|
||||
impl Drop for ZeroizingPem {
|
||||
fn drop(&mut self) {
|
||||
self.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ZeroizingPem {
|
||||
type Target = Pem;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct KeyPairPath {
|
||||
pub private_key_path: PathBuf,
|
||||
pub public_key_path: PathBuf,
|
||||
@@ -56,7 +79,7 @@ where
|
||||
if T::pem_type() != key_pem.tag {
|
||||
return Err(io::Error::other(format!(
|
||||
"unexpected key pem tag. Got '{}', expected: '{}'",
|
||||
key_pem.tag,
|
||||
key_pem.0.tag,
|
||||
T::pem_type()
|
||||
)));
|
||||
}
|
||||
@@ -77,25 +100,31 @@ where
|
||||
write_pem_file(path, key.to_bytes(), T::pem_type())
|
||||
}
|
||||
|
||||
fn read_pem_file<P: AsRef<Path>>(filepath: P) -> io::Result<Pem> {
|
||||
fn read_pem_file<P: AsRef<Path>>(filepath: P) -> io::Result<ZeroizingPem> {
|
||||
let mut pem_bytes = File::open(filepath)?;
|
||||
let mut buf = Vec::new();
|
||||
let mut buf = Zeroizing::new(Vec::new());
|
||||
pem_bytes.read_to_end(&mut buf)?;
|
||||
pem::parse(&buf).map_err(io::Error::other)
|
||||
pem::parse(&buf).map(ZeroizingPem).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
fn write_pem_file<P: AsRef<Path>>(filepath: P, data: Vec<u8>, tag: &str) -> io::Result<()> {
|
||||
fn write_pem_file<P: AsRef<Path>>(filepath: P, mut data: Vec<u8>, tag: &str) -> io::Result<()> {
|
||||
// ensure the whole directory structure exists
|
||||
if let Some(parent_dir) = filepath.as_ref().parent() {
|
||||
std::fs::create_dir_all(parent_dir)?;
|
||||
if let Err(err) = std::fs::create_dir_all(parent_dir) {
|
||||
// in case of a failure, make sure to zeroize the data before returning
|
||||
// (we can't wrap it in `Zeroize` due to `Pem` requirements)
|
||||
data.zeroize();
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
let pem = Pem {
|
||||
tag: tag.to_string(),
|
||||
contents: data,
|
||||
};
|
||||
let key = pem::encode(&pem);
|
||||
|
||||
let mut file = File::create(filepath.as_ref())?;
|
||||
|
||||
let pem = ZeroizingPem(Pem {
|
||||
tag: tag.to_string(),
|
||||
contents: data,
|
||||
});
|
||||
let key = Zeroizing::new(pem::encode(&pem));
|
||||
file.write_all(key.as_bytes())?;
|
||||
|
||||
// note: this is only supported on unix (on different systems, like Windows, it will just
|
||||
|
||||
@@ -70,6 +70,10 @@ impl ShutdownToken {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ephemeral() -> Self {
|
||||
ShutdownToken::new("ephemeral-token")
|
||||
}
|
||||
|
||||
// Creates a ShutdownToken which will get cancelled whenever the current token gets cancelled.
|
||||
// Unlike a cloned/forked ShutdownToken, cancelling a child token does not cancel the parent token.
|
||||
#[must_use]
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
use ::serde::{Deserialize, Serialize};
|
||||
use nym_api_requests::nym_nodes::SkimmedNode;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_mixnet_contract_common::EpochId;
|
||||
use nym_sphinx_addressing::nodes::NodeIdentity;
|
||||
use nym_sphinx_types::Node as SphinxNode;
|
||||
use rand::prelude::IteratorRandom;
|
||||
@@ -91,8 +93,39 @@ mod deprecated_network_address_impls {
|
||||
|
||||
pub type MixLayer = u8;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub struct NymTopologyMetadata {
|
||||
key_rotation_id: u32,
|
||||
// we have to keep track of key rotation id anyway, so we might as well also include the epoch id
|
||||
// to keep track of the data staleness
|
||||
absolute_epoch_id: EpochId,
|
||||
}
|
||||
|
||||
impl NymTopologyMetadata {
|
||||
pub fn new(key_rotation_id: u32, absolute_epoch_id: EpochId) -> Self {
|
||||
NymTopologyMetadata {
|
||||
key_rotation_id,
|
||||
absolute_epoch_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NymTopologyMetadata {
|
||||
fn default() -> Self {
|
||||
// that's not ideal, but we don't want to break backwards compatibility : /
|
||||
NymTopologyMetadata {
|
||||
key_rotation_id: u32::MAX,
|
||||
absolute_epoch_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct NymTopology {
|
||||
// while this is not ideal, use empty values as default to not break backwards compatibility
|
||||
#[serde(default)]
|
||||
metadata: NymTopologyMetadata,
|
||||
|
||||
// for the purposes of future VRF, everyone will need the same view of the network, regardless of performance filtering
|
||||
// so we use the same 'master' rewarded set information for that
|
||||
//
|
||||
@@ -128,6 +161,14 @@ impl NymRouteProvider {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_key_rotation(&self) -> u32 {
|
||||
self.topology.metadata.key_rotation_id
|
||||
}
|
||||
|
||||
pub fn absolute_epoch_id(&self) -> EpochId {
|
||||
self.topology.metadata.absolute_epoch_id
|
||||
}
|
||||
|
||||
pub fn new_empty(ignore_egress_epoch_roles: bool) -> NymRouteProvider {
|
||||
let this: Self = NymTopology::default().into();
|
||||
this.with_ignore_egress_epoch_roles(ignore_egress_epoch_roles)
|
||||
@@ -201,18 +242,22 @@ impl NymRouteProvider {
|
||||
}
|
||||
|
||||
impl NymTopology {
|
||||
#[deprecated]
|
||||
pub fn new_empty(rewarded_set: impl Into<CachedEpochRewardedSet>) -> Self {
|
||||
NymTopology {
|
||||
metadata: NymTopologyMetadata::default(),
|
||||
rewarded_set: rewarded_set.into(),
|
||||
node_details: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
metadata: NymTopologyMetadata,
|
||||
rewarded_set: impl Into<CachedEpochRewardedSet>,
|
||||
node_details: Vec<RoutingNode>,
|
||||
) -> Self {
|
||||
NymTopology {
|
||||
metadata,
|
||||
rewarded_set: rewarded_set.into(),
|
||||
node_details: node_details.into_iter().map(|n| (n.node_id, n)).collect(),
|
||||
}
|
||||
@@ -228,6 +273,11 @@ impl NymTopology {
|
||||
self.add_additional_nodes(nodes.iter())
|
||||
}
|
||||
|
||||
pub fn with_skimmed_nodes(mut self, nodes: &[SkimmedNode]) -> Self {
|
||||
self.add_skimmed_nodes(nodes);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_routing_nodes<B: Borrow<RoutingNode>>(
|
||||
&mut self,
|
||||
nodes: impl IntoIterator<Item = B>,
|
||||
@@ -278,6 +328,12 @@ impl NymTopology {
|
||||
self.node_details.contains_key(&node_id)
|
||||
}
|
||||
|
||||
pub fn has_node(&self, identity: ed25519::PublicKey) -> bool {
|
||||
self.node_details
|
||||
.values()
|
||||
.any(|node_details| node_details.identity_key == identity)
|
||||
}
|
||||
|
||||
pub fn insert_node_details(&mut self, node_details: RoutingNode) {
|
||||
self.node_details.insert(node_details.node_id, node_details);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#![allow(clippy::empty_docs)]
|
||||
|
||||
use crate::node::{EntryDetails, RoutingNode, RoutingNodeError, SupportedRoles};
|
||||
use crate::{CachedEpochRewardedSet, NymTopology};
|
||||
use crate::{CachedEpochRewardedSet, NymTopology, NymTopologyMetadata};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
@@ -38,6 +38,8 @@ impl From<SerializableTopologyError> for JsValue {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WasmFriendlyNymTopology {
|
||||
pub metadata: NymTopologyMetadata,
|
||||
|
||||
pub rewarded_set: CachedEpochRewardedSet,
|
||||
|
||||
pub node_details: HashMap<u32, WasmFriendlyRoutingNode>,
|
||||
@@ -53,13 +55,18 @@ impl TryFrom<WasmFriendlyNymTopology> for NymTopology {
|
||||
.map(|details| details.try_into())
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
Ok(NymTopology::new(value.rewarded_set, node_details))
|
||||
Ok(NymTopology::new(
|
||||
value.metadata,
|
||||
value.rewarded_set,
|
||||
node_details,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NymTopology> for WasmFriendlyNymTopology {
|
||||
fn from(value: NymTopology) -> Self {
|
||||
WasmFriendlyNymTopology {
|
||||
metadata: value.metadata,
|
||||
rewarded_set: value.rewarded_set,
|
||||
node_details: value
|
||||
.node_details
|
||||
|
||||
@@ -84,6 +84,8 @@ pub enum TypesError {
|
||||
NotADelegationEvent,
|
||||
#[error("Unknown network - {0}")]
|
||||
UnknownNetwork(String),
|
||||
#[error("the response metadata has changed between pages")]
|
||||
InconsistentPagedMetadata,
|
||||
}
|
||||
|
||||
impl Serialize for TypesError {
|
||||
@@ -103,6 +105,9 @@ impl From<ValidatorClientError> for TypesError {
|
||||
ValidatorClientError::NyxdError(e) => e.into(),
|
||||
ValidatorClientError::NoAPIUrlAvailable => TypesError::NoNymApiUrlConfigured,
|
||||
ValidatorClientError::TendermintErrorRpc(err) => err.into(),
|
||||
ValidatorClientError::InconsistentPagedMetadata => {
|
||||
TypesError::InconsistentPagedMetadata
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use nym_client_core::init::{
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_topology::wasm_helpers::WasmFriendlyNymTopology;
|
||||
use nym_topology::{NymTopology, RoutingNode};
|
||||
use nym_topology::{NymTopology, NymTopologyMetadata, RoutingNode};
|
||||
use nym_validator_client::client::IdentityKey;
|
||||
use nym_validator_client::{NymApiClient, UserAgent};
|
||||
use rand::thread_rng;
|
||||
@@ -29,7 +29,7 @@ use wasm_bindgen_futures::future_to_promise;
|
||||
use wasm_utils::error::PromisableResult;
|
||||
|
||||
pub use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
|
||||
use wasm_utils::console_log;
|
||||
use wasm_utils::{console_log, console_warn};
|
||||
|
||||
// don't get too excited about the name, under the hood it's just a big fat placeholder
|
||||
// with no disk_persistence
|
||||
@@ -73,14 +73,27 @@ pub async fn current_network_topology_async(
|
||||
|
||||
let api_client = NymApiClient::new(url);
|
||||
let rewarded_set = api_client.get_current_rewarded_set().await?;
|
||||
let mixnodes = api_client
|
||||
.get_all_basic_active_mixing_assigned_nodes()
|
||||
let mixnodes_res = api_client
|
||||
.get_all_basic_active_mixing_assigned_nodes_with_metadata()
|
||||
.await?;
|
||||
let gateways = api_client.get_all_basic_entry_assigned_nodes().await?;
|
||||
let metadata = mixnodes_res.metadata;
|
||||
let mixnodes = mixnodes_res.nodes;
|
||||
|
||||
let mut topology = NymTopology::new_empty(rewarded_set);
|
||||
topology.add_skimmed_nodes(&mixnodes);
|
||||
topology.add_skimmed_nodes(&gateways);
|
||||
let gateways_res = api_client.get_all_basic_entry_assigned_nodes_v2().await?;
|
||||
if gateways_res.metadata != metadata {
|
||||
console_warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
|
||||
return Err(WasmCoreError::UnavailableNetworkTopology);
|
||||
}
|
||||
|
||||
let gateways = gateways_res.nodes;
|
||||
|
||||
let topology = NymTopology::new(
|
||||
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
|
||||
rewarded_set,
|
||||
Vec::new(),
|
||||
)
|
||||
.with_skimmed_nodes(&mixnodes)
|
||||
.with_skimmed_nodes(&gateways);
|
||||
|
||||
Ok(topology.into())
|
||||
}
|
||||
|
||||
Generated
+1
-3
@@ -1193,7 +1193,6 @@ version = "1.5.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bs58",
|
||||
"cosmwasm-derive",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
@@ -1208,8 +1207,6 @@ dependencies = [
|
||||
"rand_chacha",
|
||||
"semver",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1262,6 +1259,7 @@ version = "0.3.0"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"tracing",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -55,3 +55,13 @@ sylvia = "1.3.3"
|
||||
schemars = "0.8.16"
|
||||
|
||||
thiserror = "2.0.11"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
unwrap_used = "deny"
|
||||
expect_used = "deny"
|
||||
todo = "deny"
|
||||
dbg_macro = "deny"
|
||||
exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
@@ -34,5 +34,6 @@ pub fn default_mixnet_init_msg() -> nym_mixnet_contract_common::InstantiateMsg {
|
||||
version_score_params: Default::default(),
|
||||
profit_margin: Default::default(),
|
||||
interval_operating_cost: Default::default(),
|
||||
key_validity_in_epochs: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts
|
||||
cosmwasm-schema = { workspace = true, optional = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
|
||||
cosmwasm-derive = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
@@ -41,8 +40,6 @@ cw-storage-plus = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
||||
semver = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
time = { version = "0.3", features = ["macros"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -55,3 +52,6 @@ easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
default = []
|
||||
contract-testing = ["mixnet-contract-common/contract-testing"]
|
||||
schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -41,6 +41,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"key_validity_in_epochs": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"profit_margin": {
|
||||
"default": {
|
||||
"maximum": "1",
|
||||
@@ -3440,6 +3449,34 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Gets the current state config of the key rotation (i.e. starting epoch id and validity duration)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_key_rotation_state"
|
||||
],
|
||||
"properties": {
|
||||
"get_key_rotation_state": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Gets the current key rotation id",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_key_rotation_id"
|
||||
],
|
||||
"properties": {
|
||||
"get_key_rotation_id": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
@@ -5116,6 +5153,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_key_rotation_id": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "KeyRotationIdResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"rotation_id"
|
||||
],
|
||||
"properties": {
|
||||
"rotation_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"get_key_rotation_state": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "KeyRotationState",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"initial_epoch_id",
|
||||
"validity_epochs"
|
||||
],
|
||||
"properties": {
|
||||
"initial_epoch_id": {
|
||||
"description": "Records the initial epoch_id when the key rotation has been introduced (0 for fresh contracts). It is used for determining when rotation is meant to advance.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"validity_epochs": {
|
||||
"description": "Defines how long each key rotation is valid for (in terms of epochs)",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"get_mix_node_bonds": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedMixnodeBondsResponse",
|
||||
|
||||
@@ -37,6 +37,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"key_validity_in_epochs": {
|
||||
"default": null,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"profit_margin": {
|
||||
"default": {
|
||||
"maximum": "1",
|
||||
|
||||
@@ -1486,6 +1486,34 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Gets the current state config of the key rotation (i.e. starting epoch id and validity duration)",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_key_rotation_state"
|
||||
],
|
||||
"properties": {
|
||||
"get_key_rotation_state": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Gets the current key rotation id",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_key_rotation_id"
|
||||
],
|
||||
"properties": {
|
||||
"get_key_rotation_id": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "KeyRotationIdResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"rotation_id"
|
||||
],
|
||||
"properties": {
|
||||
"rotation_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "KeyRotationState",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"initial_epoch_id",
|
||||
"validity_epochs"
|
||||
],
|
||||
"properties": {
|
||||
"initial_epoch_id": {
|
||||
"description": "Records the initial epoch_id when the key rotation has been introduced (0 for fresh contracts). It is used for determining when rotation is meant to advance.",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"validity_epochs": {
|
||||
"description": "Defines how long each key rotation is valid for (in terms of epochs)",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -78,6 +78,7 @@ pub const NYMNODE_ROLES_ASSIGNMENT_NAMESPACE: &str = "roles";
|
||||
pub const NYMNODE_REWARDED_SET_METADATA_NAMESPACE: &str = "roles_metadata";
|
||||
pub const NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY: &str = "active_roles";
|
||||
|
||||
pub const KEY_ROTATION_STATE_KEY: &str = "key_rot_state";
|
||||
pub const NODE_ID_COUNTER_KEY: &str = "nic";
|
||||
pub const PENDING_MIXNODE_CHANGES_NAMESPACE: &str = "pmc";
|
||||
pub const MIXNODES_PK_NAMESPACE: &str = "mnn";
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::constants::INITIAL_PLEDGE_AMOUNT;
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::nodes::storage as nymnodes_storage;
|
||||
use crate::queued_migrations::introduce_key_rotation_id;
|
||||
use crate::rewards::storage::RewardingStorage;
|
||||
use cosmwasm_std::{
|
||||
entry_point, to_json_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse,
|
||||
@@ -82,6 +83,11 @@ pub fn instantiate(
|
||||
});
|
||||
}
|
||||
|
||||
let key_rotation_validity = msg.key_validity_in_epochs();
|
||||
if key_rotation_validity < InstantiateMsg::MIN_KEY_ROTATION_VALIDITY {
|
||||
return Err(MixnetContractError::TooShortRotationInterval);
|
||||
}
|
||||
|
||||
let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?;
|
||||
let vesting_contract_address = deps.api.addr_validate(&msg.vesting_contract_address)?;
|
||||
let state = default_initial_state(
|
||||
@@ -109,7 +115,7 @@ pub fn instantiate(
|
||||
msg.current_nym_node_version,
|
||||
)?;
|
||||
RewardingStorage::new().initialise(deps.storage, reward_params)?;
|
||||
nymnodes_storage::initialise_storage(deps.storage)?;
|
||||
nymnodes_storage::initialise_storage(deps.storage, key_rotation_validity)?;
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
set_build_information!(deps.storage)?;
|
||||
|
||||
@@ -598,6 +604,14 @@ pub fn query(
|
||||
QueryMsg::GetSigningNonce { address } => to_json_binary(
|
||||
&crate::signing::queries::query_current_signing_nonce(deps, address)?,
|
||||
),
|
||||
|
||||
// sphinx key rotation-related
|
||||
QueryMsg::GetKeyRotationState {} => {
|
||||
to_json_binary(&crate::nodes::queries::query_key_rotation_state(deps)?)
|
||||
}
|
||||
QueryMsg::GetKeyRotationId {} => {
|
||||
to_json_binary(&crate::nodes::queries::query_key_rotation_id(deps)?)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(query_res?)
|
||||
@@ -605,18 +619,18 @@ pub fn query(
|
||||
|
||||
#[entry_point]
|
||||
pub fn migrate(
|
||||
deps: DepsMut<'_>,
|
||||
mut deps: DepsMut<'_>,
|
||||
_env: Env,
|
||||
msg: MigrateMsg,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
set_build_information!(deps.storage)?;
|
||||
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
// let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false);
|
||||
//
|
||||
// if !skip_state_updates {
|
||||
//
|
||||
// }
|
||||
let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false);
|
||||
|
||||
if !skip_state_updates {
|
||||
introduce_key_rotation_id(deps.branch())?;
|
||||
}
|
||||
|
||||
// due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address
|
||||
// and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh
|
||||
@@ -681,6 +695,7 @@ mod tests {
|
||||
minimum: "1000".parse().unwrap(),
|
||||
maximum: "10000".parse().unwrap(),
|
||||
},
|
||||
key_validity_in_epochs: None,
|
||||
};
|
||||
|
||||
let sender = message_info(&deps.api.addr_make("sender"), &[]);
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::constants::{
|
||||
NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT,
|
||||
UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT, UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT,
|
||||
};
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::nodes::helpers::{
|
||||
attach_nym_node_details, get_node_details_by_id, get_node_details_by_identity,
|
||||
get_node_details_by_owner,
|
||||
@@ -21,7 +22,9 @@ use mixnet_contract_common::nym_node::{
|
||||
PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse,
|
||||
StakeSaturationResponse, UnbondedNodeResponse,
|
||||
};
|
||||
use mixnet_contract_common::{NodeId, NymNodeBond, NymNodeDetails};
|
||||
use mixnet_contract_common::{
|
||||
KeyRotationIdResponse, KeyRotationState, NodeId, NymNodeBond, NymNodeDetails,
|
||||
};
|
||||
use nym_contracts_common::IdentityKey;
|
||||
|
||||
pub(crate) fn query_nymnode_bonds_paged(
|
||||
@@ -257,3 +260,14 @@ pub fn query_stake_saturation(
|
||||
uncapped_saturation: Some(node_rewarding.uncapped_bond_saturation(&rewarding_params)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_key_rotation_state(deps: Deps<'_>) -> StdResult<KeyRotationState> {
|
||||
storage::KEY_ROTATION_STATE.load(deps.storage)
|
||||
}
|
||||
|
||||
pub fn query_key_rotation_id(deps: Deps<'_>) -> StdResult<KeyRotationIdResponse> {
|
||||
let interval = interval_storage::current_interval(deps.storage)?;
|
||||
let rotation_state = storage::KEY_ROTATION_STATE.load(deps.storage)?;
|
||||
let rotation_id = rotation_state.key_rotation_id(interval.current_epoch_absolute_id());
|
||||
Ok(KeyRotationIdResponse { rotation_id })
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA};
|
||||
use crate::nodes::storage::{nym_nodes, NYMNODE_ID_COUNTER};
|
||||
use crate::nodes::storage::{nym_nodes, KEY_ROTATION_STATE, NYMNODE_ID_COUNTER};
|
||||
use cosmwasm_std::{StdResult, Storage};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role};
|
||||
use mixnet_contract_common::{EpochId, NodeId, NymNodeBond, RoleAssignment};
|
||||
use mixnet_contract_common::{EpochId, KeyRotationState, NodeId, NymNodeBond, RoleAssignment};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)]
|
||||
@@ -103,7 +103,10 @@ pub(crate) fn next_nymnode_id_counter(store: &mut dyn Storage) -> StdResult<Node
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), MixnetContractError> {
|
||||
pub(crate) fn initialise_storage(
|
||||
storage: &mut dyn Storage,
|
||||
key_rotation_validity: u32,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
let active_bucket = RoleStorageBucket::default();
|
||||
let inactive_bucket = active_bucket.other();
|
||||
|
||||
@@ -124,6 +127,15 @@ pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), Mixnet
|
||||
ROLES_METADATA.save(storage, active_bucket as u8, &Default::default())?;
|
||||
ROLES_METADATA.save(storage, inactive_bucket as u8, &Default::default())?;
|
||||
|
||||
// since we're initialising fresh storage, the current epoch_id is 0
|
||||
KEY_ROTATION_STATE.save(
|
||||
storage,
|
||||
&KeyRotationState {
|
||||
validity_epochs: key_rotation_validity,
|
||||
initial_epoch_id: 0,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,23 +2,26 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::constants::{
|
||||
NODE_ID_COUNTER_KEY, NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY, NYMNODE_IDENTITY_IDX_NAMESPACE,
|
||||
NYMNODE_OWNER_IDX_NAMESPACE, NYMNODE_PK_NAMESPACE, NYMNODE_REWARDED_SET_METADATA_NAMESPACE,
|
||||
NYMNODE_ROLES_ASSIGNMENT_NAMESPACE, PENDING_NYMNODE_CHANGES_NAMESPACE,
|
||||
UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE, UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE,
|
||||
UNBONDED_NYMNODE_PK_NAMESPACE,
|
||||
KEY_ROTATION_STATE_KEY, NODE_ID_COUNTER_KEY, NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY,
|
||||
NYMNODE_IDENTITY_IDX_NAMESPACE, NYMNODE_OWNER_IDX_NAMESPACE, NYMNODE_PK_NAMESPACE,
|
||||
NYMNODE_REWARDED_SET_METADATA_NAMESPACE, NYMNODE_ROLES_ASSIGNMENT_NAMESPACE,
|
||||
PENDING_NYMNODE_CHANGES_NAMESPACE, UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE,
|
||||
UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE, UNBONDED_NYMNODE_PK_NAMESPACE,
|
||||
};
|
||||
use crate::nodes::storage::helpers::RoleStorageBucket;
|
||||
use cosmwasm_std::Addr;
|
||||
use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex};
|
||||
use mixnet_contract_common::nym_node::{NymNodeBond, RewardedSetMetadata, Role, UnbondedNymNode};
|
||||
use mixnet_contract_common::{NodeId, PendingNodeChanges};
|
||||
use mixnet_contract_common::{KeyRotationState, NodeId, PendingNodeChanges};
|
||||
use nym_contracts_common::IdentityKey;
|
||||
|
||||
pub(crate) mod helpers;
|
||||
|
||||
pub(crate) use helpers::*;
|
||||
|
||||
/// Item recording the current state of the key rotation setup
|
||||
pub const KEY_ROTATION_STATE: Item<KeyRotationState> = Item::new(KEY_ROTATION_STATE_KEY);
|
||||
|
||||
// IMPORTANT NOTE: we're using the same storage key as we had for MIXNODE_ID_COUNTER,
|
||||
// so that we could start from the old values
|
||||
pub const NYMNODE_ID_COUNTER: Item<NodeId> = Item::new(NODE_ID_COUNTER_KEY);
|
||||
|
||||
@@ -1,2 +1,21 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::nodes::storage as nymnodes_storage;
|
||||
use cosmwasm_std::DepsMut;
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::KeyRotationState;
|
||||
|
||||
pub fn introduce_key_rotation_id(deps: DepsMut) -> Result<(), MixnetContractError> {
|
||||
let current_epoch_id =
|
||||
interval_storage::current_interval(deps.storage)?.current_epoch_absolute_id();
|
||||
nymnodes_storage::KEY_ROTATION_STATE.save(
|
||||
deps.storage,
|
||||
&KeyRotationState {
|
||||
validity_epochs: 24,
|
||||
initial_epoch_id: current_epoch_id,
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -313,6 +313,8 @@ pub(crate) fn try_update_rewarding_params(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
#[allow(clippy::unreachable)]
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// fine in test code
|
||||
#![allow(clippy::panic)]
|
||||
#![allow(clippy::unreachable)]
|
||||
#![allow(clippy::unimplemented)]
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod fixtures;
|
||||
pub(crate) mod legacy;
|
||||
@@ -1853,6 +1858,7 @@ pub mod test_helpers {
|
||||
version_score_params: Default::default(),
|
||||
profit_margin: Default::default(),
|
||||
interval_operating_cost: Default::default(),
|
||||
key_validity_in_epochs: None,
|
||||
};
|
||||
let env = mock_env();
|
||||
let info = sender("creator");
|
||||
|
||||
@@ -212,6 +212,7 @@ pub(crate) fn try_migrate_vested_delegation(
|
||||
)?))
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -314,7 +314,8 @@ impl<R, S> AuthenticatedHandler<R, S> {
|
||||
}
|
||||
Ok(request) => match request {
|
||||
// currently only a single type exists
|
||||
BinaryRequest::ForwardSphinx { packet } => {
|
||||
BinaryRequest::ForwardSphinx { packet }
|
||||
| BinaryRequest::ForwardSphinxV2 { packet } => {
|
||||
self.handle_forward_sphinx(packet).await.into_ws_message()
|
||||
}
|
||||
_ => RequestHandlingError::UnknownBinaryRequest.into_error_message(),
|
||||
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
sqlite3 -init settings.sql /Users/jedrzej/workspace/nym/target/debug/build/nym-api-84610644ac15a598/out/nym-api-example.sqlite
|
||||
@@ -23,6 +23,7 @@ thiserror.workspace = true
|
||||
time = { workspace = true, features = ["serde", "parsing", "formatting"] }
|
||||
ts-rs = { workspace = true, optional = true }
|
||||
utoipa.workspace = true
|
||||
tracing = { workspace = true }
|
||||
|
||||
# for serde on secp256k1 signatures
|
||||
ecdsa = { workspace = true, features = ["serde"] }
|
||||
|
||||
@@ -32,6 +32,7 @@ use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::{InstanceType, Schema, SchemaObject};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::net::IpAddr;
|
||||
@@ -39,8 +40,10 @@ use std::ops::{Deref, DerefMut};
|
||||
use std::{fmt, time::Duration};
|
||||
use thiserror::Error;
|
||||
use time::{Date, OffsetDateTime};
|
||||
use tracing::{error, warn};
|
||||
use utoipa::{IntoParams, ToResponse, ToSchema};
|
||||
|
||||
pub use nym_mixnet_contract_common::KeyRotationState;
|
||||
pub use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -72,7 +75,9 @@ impl Display for RequestError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, ToSchema)]
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, ToSchema, Default,
|
||||
)]
|
||||
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "generate-ts",
|
||||
@@ -86,6 +91,7 @@ pub enum MixnodeStatus {
|
||||
Active, // in both the active set and the rewarded set
|
||||
Standby, // only in the rewarded set
|
||||
Inactive, // in neither the rewarded set nor the active set, but is bonded
|
||||
#[default]
|
||||
NotFound, // doesn't even exist in the bonded set
|
||||
}
|
||||
impl MixnodeStatus {
|
||||
@@ -860,11 +866,17 @@ pub struct HostKeys {
|
||||
#[schema(value_type = String)]
|
||||
pub ed25519: ed25519::PublicKey,
|
||||
|
||||
#[deprecated(note = "use the current_x25519_sphinx_key with explicit rotation information")]
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub x25519: x25519::PublicKey,
|
||||
|
||||
pub current_x25519_sphinx_key: SphinxKey,
|
||||
|
||||
#[serde(default)]
|
||||
pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(with = "option_bs58_x25519_pubkey")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
@@ -872,11 +884,32 @@ pub struct HostKeys {
|
||||
pub x25519_noise: Option<x25519::PublicKey>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
|
||||
pub struct SphinxKey {
|
||||
pub rotation_id: u32,
|
||||
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub public_key: x25519::PublicKey,
|
||||
}
|
||||
|
||||
impl From<nym_node_requests::api::v1::node::models::SphinxKey> for SphinxKey {
|
||||
fn from(value: nym_node_requests::api::v1::node::models::SphinxKey) -> Self {
|
||||
SphinxKey {
|
||||
rotation_id: value.rotation_id,
|
||||
public_key: value.public_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys {
|
||||
fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self {
|
||||
HostKeys {
|
||||
ed25519: value.ed25519_identity,
|
||||
x25519: value.x25519_sphinx,
|
||||
current_x25519_sphinx_key: value.primary_x25519_sphinx_key.into(),
|
||||
pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key.map(Into::into),
|
||||
x25519_noise: value.x25519_noise,
|
||||
}
|
||||
}
|
||||
@@ -999,7 +1032,39 @@ impl NymNodeDescription {
|
||||
self.description.host_information.keys.ed25519
|
||||
}
|
||||
|
||||
pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode {
|
||||
pub fn current_sphinx_key(&self, current_rotation_id: u32) -> x25519::PublicKey {
|
||||
let keys = &self.description.host_information.keys;
|
||||
|
||||
if keys.current_x25519_sphinx_key.rotation_id == u32::MAX {
|
||||
// legacy case (i.e. node doesn't support rotation)
|
||||
return keys.current_x25519_sphinx_key.public_key;
|
||||
}
|
||||
|
||||
if current_rotation_id == keys.current_x25519_sphinx_key.rotation_id {
|
||||
// it's the 'current' key
|
||||
return keys.current_x25519_sphinx_key.public_key;
|
||||
}
|
||||
|
||||
if let Some(pre_announced) = &keys.pre_announced_x25519_sphinx_key {
|
||||
if pre_announced.rotation_id == current_rotation_id {
|
||||
return pre_announced.public_key;
|
||||
}
|
||||
}
|
||||
|
||||
warn!(
|
||||
"unexpected key rotation {current_rotation_id} for node {}",
|
||||
self.node_id
|
||||
);
|
||||
// this should never be reached, but just in case, return the fallback option
|
||||
keys.current_x25519_sphinx_key.public_key
|
||||
}
|
||||
|
||||
pub fn to_skimmed_node(
|
||||
&self,
|
||||
current_rotation_id: u32,
|
||||
role: NodeRole,
|
||||
performance: Performance,
|
||||
) -> SkimmedNode {
|
||||
let keys = &self.description.host_information.keys;
|
||||
let entry = if self.description.declared_role.entry {
|
||||
Some(self.entry_information())
|
||||
@@ -1012,7 +1077,7 @@ impl NymNodeDescription {
|
||||
ed25519_identity_pubkey: keys.ed25519,
|
||||
ip_addresses: self.description.host_information.ip_address.clone(),
|
||||
mix_port: self.description.mix_port(),
|
||||
x25519_sphinx_pubkey: keys.x25519,
|
||||
x25519_sphinx_pubkey: self.current_sphinx_key(current_rotation_id),
|
||||
// we can't use the declared roles, we have to take whatever was provided in the contract.
|
||||
// why? say this node COULD operate as an exit, but it might be the case the contract decided
|
||||
// to assign it an ENTRY role only. we have to use that one instead.
|
||||
@@ -1381,6 +1446,110 @@ impl NodeRefreshBody {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
|
||||
pub struct KeyRotationInfoResponse {
|
||||
pub key_rotation_state: KeyRotationState,
|
||||
|
||||
#[schema(value_type = u32)]
|
||||
pub current_absolute_epoch_id: EpochId,
|
||||
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
#[schemars(with = "String")]
|
||||
#[schema(value_type = String)]
|
||||
pub current_epoch_start: OffsetDateTime,
|
||||
|
||||
pub epoch_duration: Duration,
|
||||
}
|
||||
|
||||
impl KeyRotationInfoResponse {
|
||||
pub fn current_key_rotation_id(&self) -> u32 {
|
||||
self.key_rotation_state
|
||||
.key_rotation_id(self.current_absolute_epoch_id)
|
||||
}
|
||||
|
||||
pub fn next_rotation_starting_epoch_id(&self) -> EpochId {
|
||||
self.key_rotation_state
|
||||
.next_rotation_starting_epoch_id(self.current_absolute_epoch_id)
|
||||
}
|
||||
|
||||
pub fn current_rotation_starting_epoch_id(&self) -> EpochId {
|
||||
self.key_rotation_state
|
||||
.current_rotation_starting_epoch_id(self.current_absolute_epoch_id)
|
||||
}
|
||||
|
||||
fn current_epoch_progress(&self, now: OffsetDateTime) -> f32 {
|
||||
let elapsed = (now - self.current_epoch_start).as_seconds_f32();
|
||||
elapsed / self.epoch_duration.as_secs_f32()
|
||||
}
|
||||
|
||||
pub fn is_epoch_stuck(&self) -> bool {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let progress = self.current_epoch_progress(now);
|
||||
if progress > 1. {
|
||||
let into_next = 1. - progress;
|
||||
// if epoch hasn't progressed for more than 20% of its duration, mark is as stuck
|
||||
if into_next > 0.2 {
|
||||
let diff_time =
|
||||
Duration::from_secs_f32(into_next * self.epoch_duration.as_secs_f32());
|
||||
let expected_epoch_end = self.current_epoch_start + self.epoch_duration;
|
||||
warn!("the current epoch is expected to have been over by {expected_epoch_end}. it's already {} overdue!", humantime_serde::re::humantime::format_duration(diff_time));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
// based on the current **TIME**, determine what's the expected current rotation id
|
||||
pub fn expected_current_rotation_id(&self) -> u32 {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let current_end = now + self.epoch_duration;
|
||||
if now < current_end {
|
||||
return self
|
||||
.key_rotation_state
|
||||
.key_rotation_id(self.current_absolute_epoch_id);
|
||||
}
|
||||
|
||||
let diff = now - current_end;
|
||||
let passed_epochs = diff / self.epoch_duration;
|
||||
let expected_current_epoch = self.current_absolute_epoch_id + passed_epochs.floor() as u32;
|
||||
|
||||
self.key_rotation_state
|
||||
.key_rotation_id(expected_current_epoch)
|
||||
}
|
||||
|
||||
pub fn until_next_rotation(&self) -> Option<Duration> {
|
||||
let current_epoch_progress = self.current_epoch_progress(OffsetDateTime::now_utc());
|
||||
if current_epoch_progress > 1. {
|
||||
return None;
|
||||
}
|
||||
|
||||
let next_rotation_epoch = self.next_rotation_starting_epoch_id();
|
||||
let full_remaining =
|
||||
(next_rotation_epoch - self.current_absolute_epoch_id).checked_add(1)?;
|
||||
|
||||
let epochs_until_next_rotation = (1. - current_epoch_progress) + full_remaining as f32;
|
||||
|
||||
Some(Duration::from_secs_f32(
|
||||
epochs_until_next_rotation * self.epoch_duration.as_secs_f32(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn epoch_start_time(&self, absolute_epoch_id: EpochId) -> OffsetDateTime {
|
||||
match absolute_epoch_id.cmp(&self.current_absolute_epoch_id) {
|
||||
Ordering::Less => {
|
||||
let diff = self.current_absolute_epoch_id - absolute_epoch_id;
|
||||
self.current_epoch_start - diff * self.epoch_duration
|
||||
}
|
||||
Ordering::Equal => self.current_epoch_start,
|
||||
Ordering::Greater => {
|
||||
let diff = absolute_epoch_id - self.current_absolute_epoch_id;
|
||||
self.current_epoch_start + diff * self.epoch_duration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
|
||||
pub struct RewardedSetResponse {
|
||||
#[serde(default)]
|
||||
|
||||
@@ -8,14 +8,28 @@ use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_mixnet_contract_common::nym_node::Role;
|
||||
use nym_mixnet_contract_common::reward_params::Performance;
|
||||
use nym_mixnet_contract_common::{Interval, NodeId};
|
||||
use nym_mixnet_contract_common::{EpochId, Interval, NodeId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use time::OffsetDateTime;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
|
||||
pub struct SkimmedNodesWithMetadata {
|
||||
pub nodes: Vec<SkimmedNode>,
|
||||
pub metadata: NodesResponseMetadata,
|
||||
}
|
||||
|
||||
impl SkimmedNodesWithMetadata {
|
||||
pub fn new(nodes: Vec<SkimmedNode>, metadata: NodesResponseMetadata) -> Self {
|
||||
SkimmedNodesWithMetadata { nodes, metadata }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema, PartialEq,
|
||||
)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum TopologyRequestStatus {
|
||||
NoUpdates,
|
||||
@@ -43,20 +57,56 @@ impl<T: ToSchema> CachedNodesResponse<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema, PartialEq,
|
||||
)]
|
||||
pub struct NodesResponseMetadata {
|
||||
pub status: Option<TopologyRequestStatus>,
|
||||
#[schema(value_type = u32)]
|
||||
pub absolute_epoch_id: EpochId,
|
||||
pub rotation_id: u32,
|
||||
pub refreshed_at: OffsetDateTimeJsonSchemaWrapper,
|
||||
}
|
||||
|
||||
impl NodesResponseMetadata {
|
||||
pub fn refreshed_at(&self) -> OffsetDateTime {
|
||||
self.refreshed_at.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
|
||||
pub struct PaginatedCachedNodesResponse<T> {
|
||||
// can't add any new fields here, even with #[serde(default)] and whatnot,
|
||||
// because it will break all clients using bincode : (
|
||||
pub struct PaginatedCachedNodesResponseV1<T> {
|
||||
pub status: Option<TopologyRequestStatus>,
|
||||
pub refreshed_at: OffsetDateTimeJsonSchemaWrapper,
|
||||
pub nodes: PaginatedResponse<T>,
|
||||
}
|
||||
|
||||
impl<T> PaginatedCachedNodesResponse<T> {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
|
||||
pub struct PaginatedCachedNodesResponseV2<T> {
|
||||
pub metadata: NodesResponseMetadata,
|
||||
pub nodes: PaginatedResponse<T>,
|
||||
}
|
||||
|
||||
impl<T> From<PaginatedCachedNodesResponseV2<T>> for PaginatedCachedNodesResponseV1<T> {
|
||||
fn from(res: PaginatedCachedNodesResponseV2<T>) -> Self {
|
||||
PaginatedCachedNodesResponseV1 {
|
||||
status: res.metadata.status,
|
||||
refreshed_at: res.metadata.refreshed_at,
|
||||
nodes: res.nodes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PaginatedCachedNodesResponseV2<T> {
|
||||
pub fn new_full(
|
||||
absolute_epoch_id: EpochId,
|
||||
rotation_id: u32,
|
||||
refreshed_at: impl Into<OffsetDateTimeJsonSchemaWrapper>,
|
||||
nodes: Vec<T>,
|
||||
) -> Self {
|
||||
PaginatedCachedNodesResponse {
|
||||
refreshed_at: refreshed_at.into(),
|
||||
PaginatedCachedNodesResponseV2 {
|
||||
nodes: PaginatedResponse {
|
||||
pagination: Pagination {
|
||||
total: nodes.len(),
|
||||
@@ -65,19 +115,22 @@ impl<T> PaginatedCachedNodesResponse<T> {
|
||||
},
|
||||
data: nodes,
|
||||
},
|
||||
status: None,
|
||||
metadata: NodesResponseMetadata {
|
||||
refreshed_at: refreshed_at.into(),
|
||||
status: None,
|
||||
absolute_epoch_id,
|
||||
rotation_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fresh(mut self, interval: Option<Interval>) -> Self {
|
||||
let iv = interval.map(TopologyRequestStatus::Fresh);
|
||||
self.status = iv;
|
||||
pub fn fresh(mut self, interval: Interval) -> Self {
|
||||
self.metadata.status = Some(TopologyRequestStatus::Fresh(interval));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn no_updates() -> Self {
|
||||
PaginatedCachedNodesResponse {
|
||||
refreshed_at: OffsetDateTime::now_utc().into(),
|
||||
pub fn no_updates(absolute_epoch_id: EpochId, rotation_id: u32) -> Self {
|
||||
PaginatedCachedNodesResponseV2 {
|
||||
nodes: PaginatedResponse {
|
||||
pagination: Pagination {
|
||||
total: 0,
|
||||
@@ -86,7 +139,12 @@ impl<T> PaginatedCachedNodesResponse<T> {
|
||||
},
|
||||
data: Vec::new(),
|
||||
},
|
||||
status: Some(TopologyRequestStatus::NoUpdates),
|
||||
metadata: NodesResponseMetadata {
|
||||
refreshed_at: OffsetDateTime::now_utc().into(),
|
||||
status: Some(TopologyRequestStatus::NoUpdates),
|
||||
absolute_epoch_id,
|
||||
rotation_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.mode columns
|
||||
.headers on
|
||||
@@ -7,17 +7,19 @@ use crate::ecash::error::{EcashError, Result};
|
||||
use crate::ecash::keys::KeyPairWithEpoch;
|
||||
use crate::ecash::state::EcashState;
|
||||
use crate::network::models::NetworkDetails;
|
||||
use crate::node_describe_cache::DescribedNodes;
|
||||
use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_status_api::handlers::unstable;
|
||||
use crate::node_status_api::NodeStatusCache;
|
||||
use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::status::ApiStatusState;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use crate::support::config;
|
||||
use crate::support::http::state::{AppState, ChainStatusCache, ForcedRefresh};
|
||||
use crate::support::http::state::chain_status::ChainStatusCache;
|
||||
use crate::support::http::state::force_refresh::ForcedRefresh;
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::support::nyxd::Client;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use crate::unstable_routes::account::cache::AddressInfoCache;
|
||||
use crate::unstable_routes::v1::account::cache::AddressInfoCache;
|
||||
use async_trait::async_trait;
|
||||
use axum::Router;
|
||||
use axum_test::http::StatusCode;
|
||||
@@ -58,8 +60,8 @@ use nym_ecash_contract_common::blacklist::{BlacklistedAccountResponse, Blacklist
|
||||
use nym_ecash_contract_common::deposit::{Deposit, DepositId, DepositResponse};
|
||||
use nym_task::TaskClient;
|
||||
use nym_validator_client::nym_api::routes::{
|
||||
API_VERSION, ECASH_BLIND_SIGN, ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT,
|
||||
ECASH_ISSUED_TICKETBOOKS_FOR, ECASH_ROUTES,
|
||||
ECASH_BLIND_SIGN, ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT, ECASH_ISSUED_TICKETBOOKS_FOR,
|
||||
ECASH_ROUTES, V1_API_VERSION,
|
||||
};
|
||||
use nym_validator_client::nyxd::cosmwasm_client::logs::Log;
|
||||
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
@@ -1421,7 +1423,9 @@ impl TestFixture {
|
||||
async fn issue_ticketbook(&self, req: BlindSignRequestBody) -> BlindedSignatureResponse {
|
||||
let response = self
|
||||
.axum
|
||||
.post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}"))
|
||||
.post(&format!(
|
||||
"/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}"
|
||||
))
|
||||
.json(&req)
|
||||
.await;
|
||||
|
||||
@@ -1436,7 +1440,7 @@ impl TestFixture {
|
||||
let response = self
|
||||
.axum
|
||||
.get(&format!(
|
||||
"/{API_VERSION}/{ECASH_ROUTES}/{ECASH_ISSUED_TICKETBOOKS_FOR}/{expiration_date}"
|
||||
"/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_ISSUED_TICKETBOOKS_FOR}/{expiration_date}"
|
||||
))
|
||||
.await;
|
||||
|
||||
@@ -1452,7 +1456,7 @@ impl TestFixture {
|
||||
let dummy_keypair = ed25519::KeyPair::new(&mut OsRng);
|
||||
self.axum
|
||||
.post(&format!(
|
||||
"/{API_VERSION}/{ECASH_ROUTES}/{ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT}"
|
||||
"/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT}"
|
||||
))
|
||||
.json(
|
||||
&IssuedTicketbooksChallengeCommitmentRequestBody {
|
||||
@@ -1519,7 +1523,9 @@ mod credential_tests {
|
||||
|
||||
let response = test_fixture
|
||||
.axum
|
||||
.post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}"))
|
||||
.post(&format!(
|
||||
"/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}"
|
||||
))
|
||||
.json(&request_body)
|
||||
.await;
|
||||
|
||||
@@ -1703,7 +1709,9 @@ mod credential_tests {
|
||||
|
||||
let response = test
|
||||
.axum
|
||||
.post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}"))
|
||||
.post(&format!(
|
||||
"/{V1_API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}"
|
||||
))
|
||||
.json(&request_body)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -56,9 +56,6 @@ pub enum RewardingError {
|
||||
source: rand::distributions::WeightedError,
|
||||
},
|
||||
|
||||
#[error("could not obtain the current interval rewarding parameters")]
|
||||
RewardingParamsRetrievalFailure,
|
||||
|
||||
#[error("{0}")]
|
||||
GenericError(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// 3. Eventually this whole procedure is going to get expanded to allow for distribution of rewarded set generation
|
||||
// and hence this might be a good place for it.
|
||||
|
||||
use crate::node_describe_cache::DescribedNodes;
|
||||
use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_status_api::{NodeStatusCache, ONE_DAY};
|
||||
use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
@@ -22,7 +22,6 @@ use error::RewardingError;
|
||||
pub(crate) use helpers::RewardedNodeWithParams;
|
||||
use nym_mixnet_contract_common::{CurrentIntervalResponse, Interval};
|
||||
use nym_task::{TaskClient, TaskManager};
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{error, info, trace, warn};
|
||||
@@ -165,58 +164,6 @@ impl EpochAdvancer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// this purposely does not deal with nym-nodes as they don't have a concept of a blacklist.
|
||||
// instead clients are meant to be filtering out them themselves based on the provided scores.
|
||||
async fn update_legacy_node_blacklist(
|
||||
&mut self,
|
||||
interval: &Interval,
|
||||
) -> Result<(), RewardingError> {
|
||||
info!("Updating blacklists");
|
||||
|
||||
let mut mix_blacklist_add = HashSet::new();
|
||||
let mut mix_blacklist_remove = HashSet::new();
|
||||
let mut gate_blacklist_add = HashSet::new();
|
||||
let mut gate_blacklist_remove = HashSet::new();
|
||||
|
||||
let mixnodes = self
|
||||
.storage
|
||||
.get_all_avg_mix_reliability_in_last_24hr(interval.current_epoch_end_unix_timestamp())
|
||||
.await?;
|
||||
let gateways = self
|
||||
.storage
|
||||
.get_all_avg_gateway_reliability_in_last_24hr(
|
||||
interval.current_epoch_end_unix_timestamp(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// TODO: Make thresholds configurable
|
||||
for mix in mixnodes {
|
||||
if mix.value() <= 50.0 {
|
||||
mix_blacklist_add.insert(mix.mix_id());
|
||||
} else {
|
||||
mix_blacklist_remove.insert(mix.mix_id());
|
||||
}
|
||||
}
|
||||
|
||||
self.nym_contract_cache
|
||||
.update_mixnodes_blacklist(mix_blacklist_add, mix_blacklist_remove)
|
||||
.await;
|
||||
|
||||
for gateway in gateways {
|
||||
if gateway.value() <= 50.0 {
|
||||
gate_blacklist_add.insert(gateway.node_id());
|
||||
} else {
|
||||
gate_blacklist_remove.insert(gateway.node_id());
|
||||
}
|
||||
}
|
||||
|
||||
self.nym_contract_cache
|
||||
.update_gateways_blacklist(gate_blacklist_add, gate_blacklist_remove)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_until_epoch_end(&mut self, shutdown: &mut TaskClient) -> Option<Interval> {
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(120);
|
||||
|
||||
@@ -267,7 +214,9 @@ impl EpochAdvancer {
|
||||
|
||||
pub(crate) async fn run(&mut self, mut shutdown: TaskClient) -> Result<(), RewardingError> {
|
||||
info!("waiting for initial contract cache values before we can start rewarding");
|
||||
self.nym_contract_cache.wait_for_initial_values().await;
|
||||
self.nym_contract_cache
|
||||
.naive_wait_for_initial_values()
|
||||
.await;
|
||||
|
||||
info!("waiting for initial self-described cache values before we can start rewarding");
|
||||
self.described_cache.naive_wait_for_initial_values().await;
|
||||
@@ -278,10 +227,7 @@ impl EpochAdvancer {
|
||||
None => return Ok(()),
|
||||
Some(interval) => interval,
|
||||
};
|
||||
if let Err(err) = self.update_legacy_node_blacklist(&interval_details).await {
|
||||
error!("failed to update the node blacklist - {err}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(err) = self.perform_epoch_operations(interval_details).await {
|
||||
error!("failed to perform epoch operations - {err}");
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
|
||||
@@ -70,6 +70,8 @@ impl EpochAdvancer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// SAFETY: `EpochAdvancer` is not started until cache is properly initialised
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub(crate) async fn nodes_to_reward(
|
||||
&self,
|
||||
) -> Result<Vec<RewardedNodeWithParams>, RewardingError> {
|
||||
@@ -82,22 +84,20 @@ impl EpochAdvancer {
|
||||
self.nym_contract_cache
|
||||
.rewarded_set_owned()
|
||||
.await
|
||||
.into_inner()
|
||||
.unwrap()
|
||||
.into()
|
||||
}
|
||||
};
|
||||
|
||||
// we only need reward parameters for active set work factor and rewarded/active set sizes;
|
||||
// we do not need exact values of reward pool, staking supply, etc., so it's fine if it's slightly out of sync
|
||||
let Some(reward_params) = self
|
||||
|
||||
// SAFETY: `EpochAdvancer` is not started until cache is properly initialised
|
||||
let reward_params = self
|
||||
.nym_contract_cache
|
||||
.interval_reward_params()
|
||||
.await
|
||||
.into_inner()
|
||||
else {
|
||||
error!("failed to obtain the current interval rewarding parameters. can't determine rewards without them");
|
||||
return Err(RewardingError::RewardingParamsRetrievalFailure);
|
||||
};
|
||||
.unwrap();
|
||||
|
||||
Ok(self
|
||||
.load_nodes_for_rewarding(&rewarded_set, reward_params)
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::support::caching::refresher::{CacheUpdateWatcher, RefreshRequester};
|
||||
use nym_mixnet_contract_common::{Interval, KeyRotationState};
|
||||
use nym_task::TaskClient;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{debug, error, info, trace};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ContractData {
|
||||
interval: Interval,
|
||||
key_rotation_state: KeyRotationState,
|
||||
}
|
||||
|
||||
impl ContractData {
|
||||
fn rotation_id(&self) -> u32 {
|
||||
self.key_rotation_state
|
||||
.key_rotation_id(self.interval.current_epoch_absolute_id())
|
||||
}
|
||||
|
||||
fn upcoming_rotation_id(&self) -> u32 {
|
||||
self.rotation_id() + 1
|
||||
}
|
||||
|
||||
fn current_epoch_progress(&self, now: OffsetDateTime) -> f32 {
|
||||
let elapsed = (now - self.interval.current_epoch_start()).as_seconds_f32();
|
||||
elapsed / self.interval.epoch_length().as_secs_f32()
|
||||
}
|
||||
|
||||
fn epochs_until_next_rotation(&self) -> Option<f32> {
|
||||
let current_epoch_progress = self.current_epoch_progress(OffsetDateTime::now_utc());
|
||||
|
||||
if !(0. ..=1.).contains(¤t_epoch_progress) {
|
||||
error!("epoch seems to be stuck (current progress is at {:.1}%) - can't progress key rotation!", current_epoch_progress * 100.);
|
||||
return None;
|
||||
}
|
||||
|
||||
let next_rotation_epoch = self
|
||||
.key_rotation_state
|
||||
.next_rotation_starting_epoch_id(self.interval.current_epoch_absolute_id());
|
||||
|
||||
let Some(full_epochs) =
|
||||
(next_rotation_epoch - self.interval.current_epoch_absolute_id()).checked_sub(1)
|
||||
else {
|
||||
error!("CRITICAL FAILURE: invalid epoch calculation");
|
||||
return None;
|
||||
};
|
||||
|
||||
Some((1. - current_epoch_progress) + full_epochs as f32)
|
||||
}
|
||||
}
|
||||
|
||||
// 'simple' task responsible for making sure nym-api refreshes its self-described cache
|
||||
// just before the next key rotation so it would have all the keys available
|
||||
pub(crate) struct KeyRotationController {
|
||||
pub(crate) last_described_refreshed_for: Option<u32>,
|
||||
|
||||
pub(crate) describe_cache_refresher: RefreshRequester,
|
||||
pub(crate) contract_cache_watcher: CacheUpdateWatcher,
|
||||
pub(crate) contract_cache: NymContractCache,
|
||||
}
|
||||
|
||||
impl KeyRotationController {
|
||||
pub(crate) fn new(
|
||||
describe_cache_refresher: RefreshRequester,
|
||||
contract_cache_watcher: CacheUpdateWatcher,
|
||||
contract_cache: NymContractCache,
|
||||
) -> KeyRotationController {
|
||||
KeyRotationController {
|
||||
last_described_refreshed_for: None,
|
||||
describe_cache_refresher,
|
||||
contract_cache_watcher,
|
||||
contract_cache,
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: this function is only called after cache has already been initialised
|
||||
#[allow(clippy::unwrap_used)]
|
||||
async fn get_contract_data(&self) -> ContractData {
|
||||
let key_rotation_state = self.contract_cache.get_key_rotation_state().await.unwrap();
|
||||
let interval = self.contract_cache.current_interval().await.unwrap();
|
||||
ContractData {
|
||||
interval,
|
||||
key_rotation_state,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_contract_cache_update(&mut self) {
|
||||
let updated = self.get_contract_data().await;
|
||||
|
||||
info!(
|
||||
"current rotation: {}",
|
||||
updated
|
||||
.key_rotation_state
|
||||
.key_rotation_id(updated.interval.current_epoch_absolute_id())
|
||||
);
|
||||
|
||||
// if we're only 1/4 epoch away from the next rotation, and we haven't yet performed the refresh,
|
||||
// update the self-described cache, as all nodes should have already pre-announced their new sphinx keys
|
||||
if let Some(remaining) = updated.epochs_until_next_rotation() {
|
||||
debug!("{remaining} epoch(s) remaining until next key rotation");
|
||||
let expected = Some(updated.upcoming_rotation_id());
|
||||
if remaining < 0.25 && self.last_described_refreshed_for != expected {
|
||||
info!("{remaining} epoch(s) remaining until next key rotation - requesting full refresh of self-described cache");
|
||||
self.describe_cache_refresher.request_cache_refresh();
|
||||
self.last_described_refreshed_for = expected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(&mut self, mut task_client: TaskClient) {
|
||||
self.contract_cache.naive_wait_for_initial_values().await;
|
||||
self.handle_contract_cache_update().await;
|
||||
|
||||
while !task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = task_client.recv() => {
|
||||
trace!("KeyRotationController: Received shutdown");
|
||||
}
|
||||
_ = self.contract_cache_watcher.changed() => {
|
||||
self.handle_contract_cache_update().await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trace!("KeyRotationController: exiting")
|
||||
}
|
||||
|
||||
pub(crate) fn start(mut self, task_client: TaskClient) {
|
||||
tokio::spawn(async move { self.run(task_client).await });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::Timestamp;
|
||||
use std::time::Duration;
|
||||
|
||||
// Sun Jun 15 2025 15:06:40 GMT+0000
|
||||
const DUMMY_TIMESTAMP: i64 = 1750000000;
|
||||
|
||||
fn dummy_contract_data() -> ContractData {
|
||||
let mut env = mock_env();
|
||||
|
||||
env.block.time = Timestamp::from_seconds(DUMMY_TIMESTAMP as u64);
|
||||
ContractData {
|
||||
interval: Interval::init_interval(24, Duration::from_secs(60 * 60), &env),
|
||||
key_rotation_state: KeyRotationState {
|
||||
validity_epochs: 0,
|
||||
initial_epoch_id: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_epoch_progress() {
|
||||
let dummy_data = dummy_contract_data();
|
||||
|
||||
let epoch_start = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP).unwrap();
|
||||
let quarter_in = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP + 15 * 60).unwrap();
|
||||
let half_in = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP + 30 * 60).unwrap();
|
||||
let next = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP + 60 * 60).unwrap();
|
||||
let one_and_half = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP + 90 * 60).unwrap();
|
||||
let past_value = OffsetDateTime::from_unix_timestamp(DUMMY_TIMESTAMP - 30 * 60).unwrap();
|
||||
|
||||
assert_eq!(dummy_data.current_epoch_progress(epoch_start), 0.);
|
||||
assert_eq!(dummy_data.current_epoch_progress(quarter_in), 0.25);
|
||||
assert_eq!(dummy_data.current_epoch_progress(half_in), 0.5);
|
||||
assert_eq!(dummy_data.current_epoch_progress(next), 1.);
|
||||
assert_eq!(dummy_data.current_epoch_progress(one_and_half), 1.5);
|
||||
assert_eq!(dummy_data.current_epoch_progress(past_value), -0.5);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use tracing::{info, trace};
|
||||
mod circulating_supply_api;
|
||||
mod ecash;
|
||||
mod epoch_operations;
|
||||
mod key_rotation;
|
||||
pub(crate) mod network;
|
||||
mod network_monitor;
|
||||
pub(crate) mod node_describe_cache;
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::network_monitor::monitor::receiver::{
|
||||
use crate::network_monitor::monitor::sender::PacketSender;
|
||||
use crate::network_monitor::monitor::summary_producer::SummaryProducer;
|
||||
use crate::network_monitor::monitor::Monitor;
|
||||
use crate::node_describe_cache::DescribedNodes;
|
||||
use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_status_api::NodeStatusCache;
|
||||
use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::storage::NymApiStorage;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
use crate::network_monitor::monitor::sender::GatewayPackets;
|
||||
use crate::network_monitor::test_route::TestRoute;
|
||||
use crate::node_describe_cache::{DescribedNodes, NodeDescriptionTopologyExt};
|
||||
use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_describe_cache::NodeDescriptionTopologyExt;
|
||||
use crate::node_status_api::NodeStatusCache;
|
||||
use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
@@ -155,16 +156,9 @@ impl PacketPreparer {
|
||||
|
||||
pub(crate) async fn wait_for_validator_cache_initial_values(&self, minimum_full_routes: usize) {
|
||||
// wait for the caches to get initialised
|
||||
self.contract_cache.wait_for_initial_values().await;
|
||||
self.contract_cache.naive_wait_for_initial_values().await;
|
||||
self.described_cache.naive_wait_for_initial_values().await;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
let described_nodes = self
|
||||
.described_cache
|
||||
.get()
|
||||
.await
|
||||
.expect("the self-describe cache should have been initialised!");
|
||||
|
||||
// now wait for at least `minimum_full_routes` mixnodes per layer and `minimum_full_routes` gateway to be online
|
||||
info!("Waiting for minimal topology to be online");
|
||||
let initialisation_backoff = Duration::from_secs(30);
|
||||
@@ -173,6 +167,13 @@ impl PacketPreparer {
|
||||
let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await;
|
||||
let nym_nodes = self.contract_cache.nym_nodes().await;
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
let described_nodes = self
|
||||
.described_cache
|
||||
.get()
|
||||
.await
|
||||
.expect("the self-describe cache should have been initialised!");
|
||||
|
||||
let mut gateways_count = gateways.len();
|
||||
let mut mixnodes_count = mixnodes.len();
|
||||
|
||||
@@ -285,13 +286,16 @@ impl PacketPreparer {
|
||||
fn to_legacy_layered_mixes<'a, R: Rng>(
|
||||
&self,
|
||||
rng: &mut R,
|
||||
current_rotation_id: u32,
|
||||
node_statuses: &HashMap<NodeId, NodeAnnotation>,
|
||||
mixing_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + 'a,
|
||||
) -> HashMap<LegacyMixLayer, Vec<(RoutingNode, f64)>> {
|
||||
let mut layered_mixes = HashMap::new();
|
||||
|
||||
for mixing_nym_node in mixing_nym_nodes {
|
||||
let Some(parsed_node) = self.nym_node_to_routing_node(mixing_nym_node) else {
|
||||
let Some(parsed_node) =
|
||||
self.nym_node_to_routing_node(current_rotation_id, mixing_nym_node)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// if the node is not present, default to 0.5
|
||||
@@ -309,13 +313,16 @@ impl PacketPreparer {
|
||||
|
||||
fn to_legacy_gateway_nodes<'a>(
|
||||
&self,
|
||||
current_rotation_id: u32,
|
||||
node_statuses: &HashMap<NodeId, NodeAnnotation>,
|
||||
gateway_capable_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + 'a,
|
||||
) -> Vec<(RoutingNode, f64)> {
|
||||
let mut gateways = Vec::new();
|
||||
|
||||
for gateway_capable_node in gateway_capable_nym_nodes {
|
||||
let Some(parsed_node) = self.nym_node_to_routing_node(gateway_capable_node) else {
|
||||
let Some(parsed_node) =
|
||||
self.nym_node_to_routing_node(current_rotation_id, gateway_capable_node)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// if the node is not present, default to 0.5
|
||||
@@ -341,11 +348,21 @@ impl PacketPreparer {
|
||||
// last I checked `gatewaying` wasn't a word : )
|
||||
let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes();
|
||||
|
||||
// SAFETY: cache has already been initialised
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let current_rotation_id = self.contract_cache.current_key_rotation_id().await.unwrap();
|
||||
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// separate mixes into layers for easier selection alongside the selection weights
|
||||
let layered_mixes = self.to_legacy_layered_mixes(&mut rng, &statuses, mixing_nym_nodes);
|
||||
let gateways = self.to_legacy_gateway_nodes(&statuses, gateway_capable_nym_nodes);
|
||||
let layered_mixes = self.to_legacy_layered_mixes(
|
||||
&mut rng,
|
||||
current_rotation_id,
|
||||
&statuses,
|
||||
mixing_nym_nodes,
|
||||
);
|
||||
let gateways =
|
||||
self.to_legacy_gateway_nodes(current_rotation_id, &statuses, gateway_capable_nym_nodes);
|
||||
|
||||
// get all nodes from each layer...
|
||||
let l1 = layered_mixes.get(&LegacyMixLayer::One)?;
|
||||
@@ -399,7 +416,14 @@ impl PacketPreparer {
|
||||
let node_3 = rand_l3[i].clone();
|
||||
let gateway = rand_gateways[i].clone();
|
||||
|
||||
routes.push(TestRoute::new(rng.gen(), node_1, node_2, node_3, gateway))
|
||||
routes.push(TestRoute::new(
|
||||
rng.gen(),
|
||||
current_rotation_id,
|
||||
node_1,
|
||||
node_2,
|
||||
node_3,
|
||||
gateway,
|
||||
))
|
||||
}
|
||||
info!("The following routes will be used for testing: {routes:#?}");
|
||||
Some(routes)
|
||||
@@ -482,8 +506,12 @@ impl PacketPreparer {
|
||||
(parsed_nodes, invalid_nodes)
|
||||
}
|
||||
|
||||
fn nym_node_to_routing_node(&self, description: &NymNodeDescription) -> Option<RoutingNode> {
|
||||
description.try_to_topology_node().ok()
|
||||
fn nym_node_to_routing_node(
|
||||
&self,
|
||||
current_rotation_id: u32,
|
||||
description: &NymNodeDescription,
|
||||
) -> Option<RoutingNode> {
|
||||
description.try_to_topology_node(current_rotation_id).ok()
|
||||
}
|
||||
|
||||
pub(super) async fn prepare_test_packets(
|
||||
@@ -495,6 +523,10 @@ impl PacketPreparer {
|
||||
) -> PreparedPackets {
|
||||
let (mixnodes, gateways) = self.all_legacy_mixnodes_and_gateways().await;
|
||||
|
||||
// SAFETY: cache has already been initialised
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let current_rotation_id = self.contract_cache.current_key_rotation_id().await.unwrap();
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
let descriptions = self
|
||||
.described_cache
|
||||
@@ -521,7 +553,7 @@ impl PacketPreparer {
|
||||
|
||||
// try to add nym-nodes into the fold
|
||||
for mix in mixing_nym_nodes {
|
||||
if let Some(parsed) = self.nym_node_to_routing_node(mix) {
|
||||
if let Some(parsed) = self.nym_node_to_routing_node(current_rotation_id, mix) {
|
||||
mixnodes_under_test.push(TestableNode::new_routing(&parsed, NodeType::Mixnode));
|
||||
mixnodes_to_test_details.push(parsed);
|
||||
}
|
||||
@@ -535,7 +567,7 @@ impl PacketPreparer {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for gateway in gateway_capable_nym_nodes {
|
||||
if let Some(parsed) = self.nym_node_to_routing_node(gateway) {
|
||||
if let Some(parsed) = self.nym_node_to_routing_node(current_rotation_id, gateway) {
|
||||
gateways_under_test.push(TestableNode::new_routing(&parsed, NodeType::Gateway));
|
||||
gateways_to_test_details.push(parsed);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use nym_crypto::asymmetric::ed25519;
|
||||
use nym_mixnet_contract_common::nym_node::Role;
|
||||
use nym_mixnet_contract_common::{EpochId, EpochRewardedSet, RewardedSet};
|
||||
use nym_topology::node::RoutingNode;
|
||||
use nym_topology::{NymRouteProvider, NymTopology};
|
||||
use nym_topology::{NymRouteProvider, NymTopology, NymTopologyMetadata};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -19,6 +19,7 @@ pub(crate) struct TestRoute {
|
||||
impl TestRoute {
|
||||
pub(crate) fn new(
|
||||
id: u64,
|
||||
key_rotation_id: u32,
|
||||
l1_mix: RoutingNode,
|
||||
l2_mix: RoutingNode,
|
||||
l3_mix: RoutingNode,
|
||||
@@ -40,7 +41,11 @@ impl TestRoute {
|
||||
|
||||
TestRoute {
|
||||
id,
|
||||
nodes: NymTopology::new(fake_rewarded_set, nodes),
|
||||
nodes: NymTopology::new(
|
||||
NymTopologyMetadata::new(key_rotation_id, 0),
|
||||
fake_rewarded_set,
|
||||
nodes,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription};
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DescribedNodes {
|
||||
pub(crate) nodes: HashMap<NodeId, NymNodeDescription>,
|
||||
pub(crate) addresses_cache: HashMap<IpAddr, NodeId>,
|
||||
}
|
||||
|
||||
impl DescribedNodes {
|
||||
pub fn force_update(&mut self, node: NymNodeDescription) {
|
||||
for ip in &node.description.host_information.ip_address {
|
||||
self.addresses_cache.insert(*ip, node.node_id);
|
||||
}
|
||||
self.nodes.insert(node.node_id, node);
|
||||
}
|
||||
|
||||
pub fn get_description(&self, node_id: &NodeId) -> Option<&NymNodeData> {
|
||||
self.nodes.get(node_id).map(|n| &n.description)
|
||||
}
|
||||
|
||||
pub fn get_node(&self, node_id: &NodeId) -> Option<&NymNodeDescription> {
|
||||
self.nodes.get(node_id)
|
||||
}
|
||||
|
||||
pub fn all_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
|
||||
self.nodes.values()
|
||||
}
|
||||
|
||||
pub fn all_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
|
||||
self.nodes
|
||||
.values()
|
||||
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
|
||||
}
|
||||
|
||||
pub fn mixing_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
|
||||
self.nodes
|
||||
.values()
|
||||
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
|
||||
.filter(|n| n.description.declared_role.mixnode)
|
||||
}
|
||||
|
||||
pub fn entry_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
|
||||
self.nodes
|
||||
.values()
|
||||
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
|
||||
.filter(|n| n.description.declared_role.entry)
|
||||
}
|
||||
|
||||
pub fn exit_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
|
||||
self.nodes
|
||||
.values()
|
||||
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
|
||||
.filter(|n| n.description.declared_role.can_operate_exit_gateway())
|
||||
}
|
||||
|
||||
pub fn node_with_address(&self, address: IpAddr) -> Option<NodeId> {
|
||||
self.addresses_cache.get(&address).copied()
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,19 @@
|
||||
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_describe_cache::query_helpers::query_for_described_data;
|
||||
use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::support::caching::cache::{SharedCache, UninitialisedCache};
|
||||
use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher};
|
||||
use crate::support::config;
|
||||
use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE;
|
||||
use async_trait::async_trait;
|
||||
use futures::{stream, StreamExt};
|
||||
use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer};
|
||||
use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription};
|
||||
use crate::support::caching::cache::UninitialisedCache;
|
||||
use nym_api_requests::models::NymNodeDescription;
|
||||
use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_mixnet_contract_common::{NodeId, NymNodeDetails};
|
||||
use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt};
|
||||
use nym_topology::node::{RoutingNode, RoutingNodeError};
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_node_requests::api::client::NymNodeApiClientError;
|
||||
use nym_topology::node::RoutingNodeError;
|
||||
use nym_topology::RoutingNode;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub(crate) mod cache;
|
||||
pub(crate) mod provider;
|
||||
mod query_helpers;
|
||||
pub(crate) mod refresh;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NodeDescribeCacheError {
|
||||
@@ -71,390 +62,20 @@ pub enum NodeDescribeCacheError {
|
||||
// this exists because I've been moving things around quite a lot and now the place that holds the type
|
||||
// doesn't have relevant dependencies for proper impl
|
||||
pub(crate) trait NodeDescriptionTopologyExt {
|
||||
fn try_to_topology_node(&self) -> Result<RoutingNode, RoutingNodeError>;
|
||||
fn try_to_topology_node(
|
||||
&self,
|
||||
current_rotation_id: u32,
|
||||
) -> Result<RoutingNode, RoutingNodeError>;
|
||||
}
|
||||
|
||||
impl NodeDescriptionTopologyExt for NymNodeDescription {
|
||||
fn try_to_topology_node(&self) -> Result<RoutingNode, RoutingNodeError> {
|
||||
fn try_to_topology_node(
|
||||
&self,
|
||||
current_rotation_id: u32,
|
||||
) -> Result<RoutingNode, RoutingNodeError> {
|
||||
// for the purposes of routing, performance is completely ignored,
|
||||
// so add dummy value and piggyback on existing conversion
|
||||
(&self.to_skimmed_node(Default::default(), Default::default())).try_into()
|
||||
(&self.to_skimmed_node(current_rotation_id, Default::default(), Default::default()))
|
||||
.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DescribedNodes {
|
||||
nodes: HashMap<NodeId, NymNodeDescription>,
|
||||
addresses_cache: HashMap<IpAddr, NodeId>,
|
||||
}
|
||||
|
||||
impl DescribedNodes {
|
||||
pub fn force_update(&mut self, node: NymNodeDescription) {
|
||||
for ip in &node.description.host_information.ip_address {
|
||||
self.addresses_cache.insert(*ip, node.node_id);
|
||||
}
|
||||
self.nodes.insert(node.node_id, node);
|
||||
}
|
||||
|
||||
pub fn get_description(&self, node_id: &NodeId) -> Option<&NymNodeData> {
|
||||
self.nodes.get(node_id).map(|n| &n.description)
|
||||
}
|
||||
|
||||
pub fn get_node(&self, node_id: &NodeId) -> Option<&NymNodeDescription> {
|
||||
self.nodes.get(node_id)
|
||||
}
|
||||
|
||||
pub fn all_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
|
||||
self.nodes.values()
|
||||
}
|
||||
|
||||
pub fn all_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
|
||||
self.nodes
|
||||
.values()
|
||||
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
|
||||
}
|
||||
|
||||
pub fn mixing_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
|
||||
self.nodes
|
||||
.values()
|
||||
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
|
||||
.filter(|n| n.description.declared_role.mixnode)
|
||||
}
|
||||
|
||||
pub fn entry_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
|
||||
self.nodes
|
||||
.values()
|
||||
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
|
||||
.filter(|n| n.description.declared_role.entry)
|
||||
}
|
||||
|
||||
pub fn exit_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
|
||||
self.nodes
|
||||
.values()
|
||||
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
|
||||
.filter(|n| n.description.declared_role.can_operate_exit_gateway())
|
||||
}
|
||||
|
||||
pub fn node_with_address(&self, address: IpAddr) -> Option<NodeId> {
|
||||
self.addresses_cache.get(&address).copied()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NodeDescriptionProvider {
|
||||
contract_cache: NymContractCache,
|
||||
|
||||
allow_all_ips: bool,
|
||||
batch_size: usize,
|
||||
}
|
||||
|
||||
impl NodeDescriptionProvider {
|
||||
pub(crate) fn new(
|
||||
contract_cache: NymContractCache,
|
||||
allow_all_ips: bool,
|
||||
) -> NodeDescriptionProvider {
|
||||
NodeDescriptionProvider {
|
||||
contract_cache,
|
||||
allow_all_ips,
|
||||
batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn with_batch_size(mut self, batch_size: usize) -> Self {
|
||||
self.batch_size = batch_size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_get_client(
|
||||
host: &str,
|
||||
node_id: NodeId,
|
||||
custom_port: Option<u16>,
|
||||
) -> Result<nym_node_requests::api::Client, NodeDescribeCacheError> {
|
||||
// first try the standard port in case the operator didn't put the node behind the proxy,
|
||||
// then default https (443)
|
||||
// finally default http (80)
|
||||
let mut addresses_to_try = vec![
|
||||
format!("http://{host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), // 'standard' nym-node
|
||||
format!("https://{host}"), // node behind https proxy (443)
|
||||
format!("http://{host}"), // node behind http proxy (80)
|
||||
];
|
||||
|
||||
// note: I removed 'standard' legacy mixnode port because it should now be automatically pulled via
|
||||
// the 'custom_port' since it should have been present in the contract.
|
||||
|
||||
if let Some(port) = custom_port {
|
||||
addresses_to_try.insert(0, format!("http://{host}:{port}"));
|
||||
}
|
||||
|
||||
for address in addresses_to_try {
|
||||
// if provided host was malformed, no point in continuing
|
||||
let client = match nym_node_requests::api::Client::builder(address).and_then(|b| {
|
||||
b.with_timeout(Duration::from_secs(5))
|
||||
.no_hickory_dns()
|
||||
.with_user_agent("nym-api-describe-cache")
|
||||
.build()
|
||||
}) {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
return Err(NodeDescribeCacheError::MalformedHost {
|
||||
host: host.to_string(),
|
||||
node_id,
|
||||
source: err,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(health) = client.get_health().await {
|
||||
if health.status.is_up() {
|
||||
return Ok(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(NodeDescribeCacheError::NoHttpPortsAvailable {
|
||||
host: host.to_string(),
|
||||
node_id,
|
||||
})
|
||||
}
|
||||
|
||||
async fn try_get_description(
|
||||
data: RefreshData,
|
||||
allow_all_ips: bool,
|
||||
) -> Result<NymNodeDescription, NodeDescribeCacheError> {
|
||||
let client = try_get_client(&data.host, data.node_id, data.port).await?;
|
||||
|
||||
let map_query_err = |err| NodeDescribeCacheError::ApiFailure {
|
||||
node_id: data.node_id,
|
||||
source: err,
|
||||
};
|
||||
|
||||
let host_info = client.get_host_information().await.map_err(map_query_err)?;
|
||||
|
||||
// check if the identity key matches the information provided during bonding
|
||||
if data.expected_identity != host_info.keys.ed25519_identity {
|
||||
return Err(NodeDescribeCacheError::MismatchedIdentity {
|
||||
node_id: data.node_id,
|
||||
expected: data.expected_identity.to_base58_string(),
|
||||
got: host_info.keys.ed25519_identity.to_base58_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if !host_info.verify_host_information() {
|
||||
return Err(NodeDescribeCacheError::MissignedHostInformation {
|
||||
node_id: data.node_id,
|
||||
});
|
||||
}
|
||||
|
||||
if !allow_all_ips && !host_info.data.check_ips() {
|
||||
return Err(NodeDescribeCacheError::IllegalIpAddress {
|
||||
node_id: data.node_id,
|
||||
});
|
||||
}
|
||||
|
||||
let node_info = query_for_described_data(&client, data.node_id).await?;
|
||||
let description = node_info.into_node_description(host_info.data);
|
||||
|
||||
Ok(NymNodeDescription {
|
||||
node_id: data.node_id,
|
||||
contract_node_type: data.node_type,
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RefreshData {
|
||||
host: String,
|
||||
node_id: NodeId,
|
||||
expected_identity: ed25519::PublicKey,
|
||||
node_type: DescribedNodeType,
|
||||
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a LegacyMixNodeDetailsWithLayer> for RefreshData {
|
||||
type Error = ed25519::Ed25519RecoveryError;
|
||||
|
||||
fn try_from(node: &'a LegacyMixNodeDetailsWithLayer) -> Result<Self, Self::Error> {
|
||||
Ok(RefreshData::new(
|
||||
&node.bond_information.mix_node.host,
|
||||
node.bond_information.identity().parse()?,
|
||||
DescribedNodeType::LegacyMixnode,
|
||||
node.mix_id(),
|
||||
Some(node.bond_information.mix_node.http_api_port),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a LegacyGatewayBondWithId> for RefreshData {
|
||||
type Error = ed25519::Ed25519RecoveryError;
|
||||
|
||||
fn try_from(node: &'a LegacyGatewayBondWithId) -> Result<Self, Self::Error> {
|
||||
Ok(RefreshData::new(
|
||||
&node.bond.gateway.host,
|
||||
node.bond.identity().parse()?,
|
||||
DescribedNodeType::LegacyGateway,
|
||||
node.node_id,
|
||||
None,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a NymNodeDetails> for RefreshData {
|
||||
type Error = ed25519::Ed25519RecoveryError;
|
||||
|
||||
fn try_from(node: &'a NymNodeDetails) -> Result<Self, Self::Error> {
|
||||
Ok(RefreshData::new(
|
||||
&node.bond_information.node.host,
|
||||
node.bond_information.identity().parse()?,
|
||||
DescribedNodeType::NymNode,
|
||||
node.node_id(),
|
||||
node.bond_information.node.custom_http_port,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl RefreshData {
|
||||
pub fn new(
|
||||
host: impl Into<String>,
|
||||
expected_identity: ed25519::PublicKey,
|
||||
node_type: DescribedNodeType,
|
||||
node_id: NodeId,
|
||||
port: Option<u16>,
|
||||
) -> Self {
|
||||
RefreshData {
|
||||
host: host.into(),
|
||||
node_id,
|
||||
expected_identity,
|
||||
node_type,
|
||||
port,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn node_id(&self) -> NodeId {
|
||||
self.node_id
|
||||
}
|
||||
|
||||
pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option<NymNodeDescription> {
|
||||
match try_get_description(self, allow_all_ips).await {
|
||||
Ok(description) => Some(description),
|
||||
Err(err) => {
|
||||
debug!("failed to obtain node self-described data: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CacheItemProvider for NodeDescriptionProvider {
|
||||
type Item = DescribedNodes;
|
||||
type Error = NodeDescribeCacheError;
|
||||
|
||||
async fn wait_until_ready(&self) {
|
||||
self.contract_cache.wait_for_initial_values().await
|
||||
}
|
||||
|
||||
async fn try_refresh(&self) -> Result<Self::Item, Self::Error> {
|
||||
// we need to query:
|
||||
// - legacy mixnodes (because they might already be running nym-nodes, but haven't updated contract info)
|
||||
// - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info)
|
||||
// - nym-nodes
|
||||
|
||||
let mut nodes_to_query: Vec<RefreshData> = Vec::new();
|
||||
|
||||
match self.contract_cache.all_cached_legacy_mixnodes().await {
|
||||
None => error!("failed to obtain mixnodes information from the cache"),
|
||||
Some(legacy_mixnodes) => {
|
||||
for node in &**legacy_mixnodes {
|
||||
if let Ok(data) = node.try_into() {
|
||||
nodes_to_query.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self.contract_cache.all_cached_legacy_gateways().await {
|
||||
None => error!("failed to obtain gateways information from the cache"),
|
||||
Some(legacy_gateways) => {
|
||||
for node in &**legacy_gateways {
|
||||
if let Ok(data) = node.try_into() {
|
||||
nodes_to_query.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self.contract_cache.all_cached_nym_nodes().await {
|
||||
None => error!("failed to obtain nym-nodes information from the cache"),
|
||||
Some(nym_nodes) => {
|
||||
for node in &**nym_nodes {
|
||||
if let Ok(data) = node.try_into() {
|
||||
nodes_to_query.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nodes = stream::iter(
|
||||
nodes_to_query
|
||||
.into_iter()
|
||||
.map(|n| n.try_refresh(self.allow_all_ips)),
|
||||
)
|
||||
.buffer_unordered(self.batch_size)
|
||||
.filter_map(|x| async move { x.map(|d| (d.node_id, d)) })
|
||||
.collect::<HashMap<_, _>>()
|
||||
.await;
|
||||
|
||||
let mut addresses_cache = HashMap::new();
|
||||
for node in nodes.values() {
|
||||
for ip in &node.description.host_information.ip_address {
|
||||
addresses_cache.insert(*ip, node.node_id);
|
||||
}
|
||||
}
|
||||
|
||||
info!("refreshed self described data for {} nodes", nodes.len());
|
||||
info!("with {} unique ip addresses", addresses_cache.len());
|
||||
|
||||
Ok(DescribedNodes {
|
||||
nodes,
|
||||
addresses_cache,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// currently dead code : (
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn new_refresher(
|
||||
config: &config::TopologyCacher,
|
||||
contract_cache: NymContractCache,
|
||||
) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> {
|
||||
CacheRefresher::new(
|
||||
Box::new(
|
||||
NodeDescriptionProvider::new(
|
||||
contract_cache,
|
||||
config.debug.node_describe_allow_illegal_ips,
|
||||
)
|
||||
.with_batch_size(config.debug.node_describe_batch_size),
|
||||
),
|
||||
config.debug.node_describe_caching_interval,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn new_refresher_with_initial_value(
|
||||
config: &config::TopologyCacher,
|
||||
contract_cache: NymContractCache,
|
||||
initial: SharedCache<DescribedNodes>,
|
||||
) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> {
|
||||
CacheRefresher::new_with_initial_value(
|
||||
Box::new(
|
||||
NodeDescriptionProvider::new(
|
||||
contract_cache,
|
||||
config.debug.node_describe_allow_illegal_ips,
|
||||
)
|
||||
.with_batch_size(config.debug.node_describe_batch_size),
|
||||
),
|
||||
config.debug.node_describe_caching_interval,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_describe_cache::refresh::RefreshData;
|
||||
use crate::node_describe_cache::NodeDescribeCacheError;
|
||||
use crate::nym_contract_cache::cache::NymContractCache;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher};
|
||||
use crate::support::config;
|
||||
use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE;
|
||||
use async_trait::async_trait;
|
||||
use futures::{stream, StreamExt};
|
||||
use std::collections::HashMap;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub struct NodeDescriptionProvider {
|
||||
contract_cache: NymContractCache,
|
||||
|
||||
allow_all_ips: bool,
|
||||
batch_size: usize,
|
||||
}
|
||||
|
||||
impl NodeDescriptionProvider {
|
||||
pub(crate) fn new(
|
||||
contract_cache: NymContractCache,
|
||||
allow_all_ips: bool,
|
||||
) -> NodeDescriptionProvider {
|
||||
NodeDescriptionProvider {
|
||||
contract_cache,
|
||||
allow_all_ips,
|
||||
batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn with_batch_size(mut self, batch_size: usize) -> Self {
|
||||
self.batch_size = batch_size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CacheItemProvider for NodeDescriptionProvider {
|
||||
type Item = DescribedNodes;
|
||||
type Error = NodeDescribeCacheError;
|
||||
|
||||
async fn wait_until_ready(&self) {
|
||||
self.contract_cache.naive_wait_for_initial_values().await
|
||||
}
|
||||
|
||||
async fn try_refresh(&self) -> Result<Self::Item, Self::Error> {
|
||||
// we need to query:
|
||||
// - legacy mixnodes (because they might already be running nym-nodes, but haven't updated contract info)
|
||||
// - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info)
|
||||
// - nym-nodes
|
||||
|
||||
let mut nodes_to_query: Vec<RefreshData> = Vec::new();
|
||||
|
||||
match self.contract_cache.all_cached_legacy_mixnodes().await {
|
||||
None => error!("failed to obtain mixnodes information from the cache"),
|
||||
Some(legacy_mixnodes) => {
|
||||
for node in &**legacy_mixnodes {
|
||||
if let Ok(data) = node.try_into() {
|
||||
nodes_to_query.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self.contract_cache.all_cached_legacy_gateways().await {
|
||||
None => error!("failed to obtain gateways information from the cache"),
|
||||
Some(legacy_gateways) => {
|
||||
for node in &**legacy_gateways {
|
||||
if let Ok(data) = node.try_into() {
|
||||
nodes_to_query.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self.contract_cache.all_cached_nym_nodes().await {
|
||||
None => error!("failed to obtain nym-nodes information from the cache"),
|
||||
Some(nym_nodes) => {
|
||||
for node in &**nym_nodes {
|
||||
if let Ok(data) = node.try_into() {
|
||||
nodes_to_query.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nodes = stream::iter(
|
||||
nodes_to_query
|
||||
.into_iter()
|
||||
.map(|n| n.try_refresh(self.allow_all_ips)),
|
||||
)
|
||||
.buffer_unordered(self.batch_size)
|
||||
.filter_map(|x| async move { x.map(|d| (d.node_id, d)) })
|
||||
.collect::<HashMap<_, _>>()
|
||||
.await;
|
||||
|
||||
let mut addresses_cache = HashMap::new();
|
||||
for node in nodes.values() {
|
||||
for ip in &node.description.host_information.ip_address {
|
||||
addresses_cache.insert(*ip, node.node_id);
|
||||
}
|
||||
}
|
||||
|
||||
info!("refreshed self described data for {} nodes", nodes.len());
|
||||
info!("with {} unique ip addresses", addresses_cache.len());
|
||||
|
||||
Ok(DescribedNodes {
|
||||
nodes,
|
||||
addresses_cache,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// currently dead code : (
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn new_refresher(
|
||||
config: &config::TopologyCacher,
|
||||
contract_cache: NymContractCache,
|
||||
) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> {
|
||||
CacheRefresher::new(
|
||||
Box::new(
|
||||
NodeDescriptionProvider::new(
|
||||
contract_cache,
|
||||
config.debug.node_describe_allow_illegal_ips,
|
||||
)
|
||||
.with_batch_size(config.debug.node_describe_batch_size),
|
||||
),
|
||||
config.debug.node_describe_caching_interval,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn new_provider_with_initial_value(
|
||||
config: &config::TopologyCacher,
|
||||
contract_cache: NymContractCache,
|
||||
initial: SharedCache<DescribedNodes>,
|
||||
) -> CacheRefresher<DescribedNodes, NodeDescribeCacheError> {
|
||||
CacheRefresher::new_with_initial_value(
|
||||
Box::new(
|
||||
NodeDescriptionProvider::new(
|
||||
contract_cache,
|
||||
config.debug.node_describe_allow_illegal_ips,
|
||||
)
|
||||
.with_batch_size(config.debug.node_describe_batch_size),
|
||||
),
|
||||
config.debug.node_describe_caching_interval,
|
||||
initial,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_describe_cache::query_helpers::query_for_described_data;
|
||||
use crate::node_describe_cache::NodeDescribeCacheError;
|
||||
use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer};
|
||||
use nym_api_requests::models::{DescribedNodeType, NymNodeDescription};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_mixnet_contract_common::{NodeId, NymNodeDetails};
|
||||
use nym_node_requests::api::client::NymNodeApiClientExt;
|
||||
use nym_validator_client::UserAgent;
|
||||
use std::time::Duration;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RefreshData {
|
||||
host: String,
|
||||
node_id: NodeId,
|
||||
expected_identity: ed25519::PublicKey,
|
||||
node_type: DescribedNodeType,
|
||||
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a LegacyMixNodeDetailsWithLayer> for RefreshData {
|
||||
type Error = ed25519::Ed25519RecoveryError;
|
||||
|
||||
fn try_from(node: &'a LegacyMixNodeDetailsWithLayer) -> Result<Self, Self::Error> {
|
||||
Ok(RefreshData::new(
|
||||
&node.bond_information.mix_node.host,
|
||||
node.bond_information.identity().parse()?,
|
||||
DescribedNodeType::LegacyMixnode,
|
||||
node.mix_id(),
|
||||
Some(node.bond_information.mix_node.http_api_port),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a LegacyGatewayBondWithId> for RefreshData {
|
||||
type Error = ed25519::Ed25519RecoveryError;
|
||||
|
||||
fn try_from(node: &'a LegacyGatewayBondWithId) -> Result<Self, Self::Error> {
|
||||
Ok(RefreshData::new(
|
||||
&node.bond.gateway.host,
|
||||
node.bond.identity().parse()?,
|
||||
DescribedNodeType::LegacyGateway,
|
||||
node.node_id,
|
||||
None,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a NymNodeDetails> for RefreshData {
|
||||
type Error = ed25519::Ed25519RecoveryError;
|
||||
|
||||
fn try_from(node: &'a NymNodeDetails) -> Result<Self, Self::Error> {
|
||||
Ok(RefreshData::new(
|
||||
&node.bond_information.node.host,
|
||||
node.bond_information.identity().parse()?,
|
||||
DescribedNodeType::NymNode,
|
||||
node.node_id(),
|
||||
node.bond_information.node.custom_http_port,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl RefreshData {
|
||||
pub fn new(
|
||||
host: impl Into<String>,
|
||||
expected_identity: ed25519::PublicKey,
|
||||
node_type: DescribedNodeType,
|
||||
node_id: NodeId,
|
||||
port: Option<u16>,
|
||||
) -> Self {
|
||||
RefreshData {
|
||||
host: host.into(),
|
||||
node_id,
|
||||
expected_identity,
|
||||
node_type,
|
||||
port,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn node_id(&self) -> NodeId {
|
||||
self.node_id
|
||||
}
|
||||
|
||||
pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option<NymNodeDescription> {
|
||||
match try_get_description(self, allow_all_ips).await {
|
||||
Ok(description) => Some(description),
|
||||
Err(err) => {
|
||||
debug!("failed to obtain node self-described data: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_get_client(
|
||||
host: &str,
|
||||
node_id: NodeId,
|
||||
custom_port: Option<u16>,
|
||||
) -> Result<nym_node_requests::api::Client, NodeDescribeCacheError> {
|
||||
// first try the standard port in case the operator didn't put the node behind the proxy,
|
||||
// then default https (443)
|
||||
// finally default http (80)
|
||||
let mut addresses_to_try = vec![
|
||||
format!("http://{host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), // 'standard' nym-node
|
||||
format!("https://{host}"), // node behind https proxy (443)
|
||||
format!("http://{host}"), // node behind http proxy (80)
|
||||
];
|
||||
|
||||
// note: I removed 'standard' legacy mixnode port because it should now be automatically pulled via
|
||||
// the 'custom_port' since it should have been present in the contract.
|
||||
|
||||
if let Some(port) = custom_port {
|
||||
addresses_to_try.insert(0, format!("http://{host}:{port}"));
|
||||
}
|
||||
|
||||
for address in addresses_to_try {
|
||||
// if provided host was malformed, no point in continuing
|
||||
let client = match nym_node_requests::api::Client::builder(address).and_then(|b| {
|
||||
b.with_timeout(Duration::from_secs(5))
|
||||
.no_hickory_dns()
|
||||
.with_user_agent(UserAgent::from(bin_info!()))
|
||||
.build()
|
||||
}) {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
return Err(NodeDescribeCacheError::MalformedHost {
|
||||
host: host.to_string(),
|
||||
node_id,
|
||||
source: err,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(health) = client.get_health().await {
|
||||
if health.status.is_up() {
|
||||
return Ok(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(NodeDescribeCacheError::NoHttpPortsAvailable {
|
||||
host: host.to_string(),
|
||||
node_id,
|
||||
})
|
||||
}
|
||||
|
||||
async fn try_get_description(
|
||||
data: RefreshData,
|
||||
allow_all_ips: bool,
|
||||
) -> Result<NymNodeDescription, NodeDescribeCacheError> {
|
||||
let client = try_get_client(&data.host, data.node_id, data.port).await?;
|
||||
|
||||
let map_query_err = |err| NodeDescribeCacheError::ApiFailure {
|
||||
node_id: data.node_id,
|
||||
source: err,
|
||||
};
|
||||
|
||||
let host_info = client.get_host_information().await.map_err(map_query_err)?;
|
||||
|
||||
// check if the identity key matches the information provided during bonding
|
||||
if data.expected_identity != host_info.keys.ed25519_identity {
|
||||
return Err(NodeDescribeCacheError::MismatchedIdentity {
|
||||
node_id: data.node_id,
|
||||
expected: data.expected_identity.to_base58_string(),
|
||||
got: host_info.keys.ed25519_identity.to_base58_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if !host_info.verify_host_information() {
|
||||
return Err(NodeDescribeCacheError::MissignedHostInformation {
|
||||
node_id: data.node_id,
|
||||
});
|
||||
}
|
||||
|
||||
if !allow_all_ips && !host_info.data.check_ips() {
|
||||
return Err(NodeDescribeCacheError::IllegalIpAddress {
|
||||
node_id: data.node_id,
|
||||
});
|
||||
}
|
||||
|
||||
let node_info = query_for_described_data(&client, data.node_id).await?;
|
||||
let description = node_info.into_node_description(host_info.data);
|
||||
|
||||
Ok(NymNodeDescription {
|
||||
node_id: data.node_id,
|
||||
contract_node_type: data.node_type,
|
||||
description,
|
||||
})
|
||||
}
|
||||
+18
-70
@@ -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
@@ -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
@@ -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));
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user