Compare commits

...

28 Commits

Author SHA1 Message Date
Jędrzej Stuczyński be67234093 bugfix: credential-proxy obtain-async (#5067)
* removed foreign key constraint on deposit table

* fixed sql nullability

* fixed swagger arguments for '/api/v1/ticketbook/shares/device/{device_id}/credential/{credential_id}' route

* fixed missing swagger component definitions
2024-10-31 10:33:38 +00:00
Fouad 8b0b70a727 allow nym node config updates (#5066) 2024-10-31 09:59:22 +00:00
Fouad c90ebf0a6a Feature/wallet bonding fixes (#5064)
* bonding and unbonding for nym nodes
2024-10-30 17:15:38 +00:00
Jędrzej Stuczyński 07ff2639ec bugfix: use corrext axum extractors for ecash route arguments (#5065) 2024-10-30 16:05:16 +00:00
Jędrzej Stuczyński 753a21f8ca bugfix/feature: added NymApiClient method to get all skimmed nodes (#5062)
* bugfix/feature: added NymApiClient method to get all skimmed nodes

* wasm

* helper: utility method for getting ed25519 identity directly from node description
2024-10-30 12:21:27 +00:00
Jędrzej Stuczyński 76da4ab532 bugfix: mark migrated gateways as rewarded in the previous epoch in case theyre in the rewarded set (#5049) 2024-10-30 09:11:13 +00:00
Jędrzej Stuczyński 317f7fffa9 added hacky routes to return nymnodes alongside legacy nodes (#5051)
* added hacky routes to return nymnodes alongside legacy nodes

* fixed mixing role

* Update client (#5054)

* removed hacky mixnodes endpoint for its not used

* construct explorer-api client with timeout

---------

Co-authored-by: Dinko Zdravac <173912580+dynco-nym@users.noreply.github.com>
2024-10-29 08:35:07 +00:00
Jędrzej Stuczyński 4396def133 bugfix: adjust runtime storage migration (#5047) 2024-10-28 10:07:51 +00:00
Jędrzej Stuczyński a56a318a7f bugfix: supersede 'cb13be27f8f61d9ae74d924e85d2e6787895eb14' by using query parameters (#5046) 2024-10-28 09:57:14 +00:00
Jędrzej Stuczyński 4d08047c57 bugfix: restore default http port for nym-api (#5045)
when it was run under 'rocket' server the port used was 8000. let's restore that value
2024-10-28 09:28:47 +00:00
Jędrzej Stuczyński cb13be27f8 bugfix: fix ecash handlers routes (#5043) 2024-10-28 09:12:40 +00:00
Jędrzej Stuczyński fa392169c1 bugfix: use human readable roles for annotations (#5036)
* bugfix: use human readable roles for annotations

* update the wallet code to use 'DisplayRole'
2024-10-28 09:08:17 +00:00
Jędrzej Stuczyński 3167fb34e6 bugfix: don't assign exit gateways to standby set (#5041) 2024-10-25 16:53:51 +01:00
Jędrzej Stuczyński 9ca6301e1c bugfix: make sure nym-nodes are also tested by network monitor (#5040) 2024-10-25 15:20:39 +01:00
Jędrzej Stuczyński e16a73338e bugfix: use bonded nym-nodes for determining initial network monitor nodes (#5039) 2024-10-25 12:34:25 +01:00
Bogdan-Ștefan Neacşu bfa3825d70 Pass the Poisson flag on authenticator config (#5037) 2024-10-25 14:08:52 +03:00
Jędrzej Stuczyński d626e7689f bugfix: make gateways insert themselves into [local] topology (#5038)
* added explicit SP suffix to started tasks

* added 'GatewayTopologyProvider' that always injects itself into the network

* use the new topology provider to bypass described bootstrapping problem
2024-10-25 12:06:16 +01:00
Jędrzej Stuczyński 9234474565 bugfix: use old name for 'epoch_role' in SkimmedNode (#5034)
* bugfix: use old name for 'epoch_role' in SkimmedNode

* clippy
2024-10-25 09:29:37 +01:00
Jędrzej Stuczyński 29f8386b50 bugfix: make sure to use correct highest node id when assigning role (#5032)
* bugfix: make sure to use correct highest node id when assigning role

* make sure nym-api provides sorted values for older contracts
2024-10-24 17:47:57 +01:00
Jędrzej Stuczyński 0edb9631a6 feature: use axum_client_ip for attempting to extract source ip (#5031) 2024-10-24 17:38:32 +01:00
Jędrzej Stuczyński 4b0153f5f2 bugfix: fixed backwards incompatibility for /gateways/described endpoint (#5030) 2024-10-24 15:37:41 +01:00
Jędrzej Stuczyński c09a17b66d bugfix: verifying signed information of legacy nodes (#5029)
* Added new legacy variant of HostInformation

* fixed 'option_bs58_x25519_pubkey' for empty string

* 'Debug' impl for x25519 and ed25519 to use human-readable representation

* HttpClient to use explicit 'serde_json' conversion for better errors

* additional 'Debug' derives
2024-10-24 15:00:34 +01:00
Jędrzej Stuczyński d18ddcdc11 bugfix: introduce 'LegacyPendingMixNodeChanges' that does not contain 'cost_params_change' (#5028)
* bugfix: introduce 'LegacyPendingMixNodeChanges' that does not contain 'cost_params_change'

* updated schema files due to removal of '#[serde(deny_unknown_fields)]'
2024-10-24 10:54:00 +01:00
Jędrzej Stuczyński d2df542280 bugfix: missing #[serde(default)] for announce port (#5024) 2024-10-23 16:52:17 +01:00
Jędrzej Stuczyński 6fafd8c03a bugfix: directory v2.1 get_all_avg_gateway_reliability_in_interval query (#5023)
* log full storage errors on failures

* use query_as! macro
2024-10-23 16:36:21 +01:00
Jędrzej Stuczyński 38e66f6ddf added 'get_all_described_nodes' to NymApiClient and adjusted return type on api itself (#5016) 2024-10-23 09:48:25 +01:00
Bogdan-Ștefan Neacşu b9fbe0b8f3 Reapply fixes to new branch (#5014) 2024-10-22 18:33:18 +03:00
Bogdan-Ștefan Neacşu daafb5cae4 Consume only positive bandwidth (#5013) 2024-10-22 17:46:46 +03:00
96 changed files with 1494 additions and 361 deletions
Generated
+30
View File
@@ -481,6 +481,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-client-ip"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac"
dependencies = [
"axum 0.7.7",
"forwarded-header-value",
"serde",
]
[[package]]
name = "axum-core"
version = "0.3.4"
@@ -2585,6 +2596,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e"
[[package]]
name = "forwarded-header-value"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
dependencies = [
"nonempty",
"thiserror",
]
[[package]]
name = "fs-err"
version = "2.11.0"
@@ -4198,6 +4219,12 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nonempty"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
[[package]]
name = "notify"
version = "5.2.0"
@@ -5266,9 +5293,11 @@ dependencies = [
"nym-network-requester",
"nym-node-http-api",
"nym-pemstore",
"nym-sdk",
"nym-sphinx",
"nym-statistics-common",
"nym-task",
"nym-topology",
"nym-types",
"nym-validator-client",
"nym-wireguard",
@@ -5419,6 +5448,7 @@ name = "nym-http-api-common"
version = "0.1.0"
dependencies = [
"axum 0.7.7",
"axum-client-ip",
"bytes",
"colored",
"mime",
+1
View File
@@ -186,6 +186,7 @@ aead = "0.5.2"
anyhow = "1.0.89"
argon2 = "0.5.0"
async-trait = "0.1.83"
axum-client-ip = "0.6.1"
axum = "0.7.5"
axum-extra = "0.9.4"
base64 = "0.22.1"
@@ -112,7 +112,7 @@ impl GeoAwareTopologyProvider {
async fn get_topology(&self) -> Option<NymTopology> {
let mixnodes = match self
.validator_client
.get_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.await
{
Err(err) => {
@@ -6,7 +6,6 @@ pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit};
use futures::StreamExt;
use log::*;
use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::NymTopologyError;
use std::time::Duration;
@@ -18,7 +17,11 @@ use wasmtimer::tokio::sleep;
mod accessor;
pub mod geo_aware_provider;
pub(crate) mod nym_api_provider;
pub mod nym_api_provider;
pub use geo_aware_provider::GeoAwareTopologyProvider;
pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider};
pub use nym_topology::provider_trait::TopologyProvider;
// TODO: move it to config later
const MAX_FAILURE_COUNT: usize = 10;
@@ -14,9 +14,10 @@ use url::Url;
pub const DEFAULT_MIN_MIXNODE_PERFORMANCE: u8 = 50;
pub const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50;
pub(crate) struct Config {
pub(crate) min_mixnode_performance: u8,
pub(crate) min_gateway_performance: u8,
#[derive(Debug)]
pub struct Config {
pub min_mixnode_performance: u8,
pub min_gateway_performance: u8,
}
impl Default for Config {
@@ -29,7 +30,7 @@ impl Default for Config {
}
}
pub(crate) struct NymApiTopologyProvider {
pub struct NymApiTopologyProvider {
config: Config,
validator_client: nym_validator_client::client::NymApiClient,
@@ -40,7 +41,7 @@ pub(crate) struct NymApiTopologyProvider {
}
impl NymApiTopologyProvider {
pub(crate) fn new(
pub fn new(
config: Config,
mut nym_api_urls: Vec<Url>,
client_version: String,
@@ -98,7 +99,7 @@ impl NymApiTopologyProvider {
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
let mixnodes = match self
.validator_client
.get_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone()))
.await
{
Err(err) => {
+3 -1
View File
@@ -121,7 +121,9 @@ pub async fn current_mixnodes<R: Rng>(
log::trace!("Fetching list of mixnodes from: {nym_api}");
let mixnodes = client.get_basic_active_mixing_assigned_nodes(None).await?;
let mixnodes = client
.get_all_basic_active_mixing_assigned_nodes(None)
.await?;
let valid_mixnodes = mixnodes
.iter()
.filter_map(|mixnode| mixnode.try_into().ok())
@@ -19,7 +19,7 @@ use nym_api_requests::ecash::{
};
use nym_api_requests::models::{
GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse,
RewardEstimationResponse, StakeSaturationResponse,
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
};
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::nym_nodes::SkimmedNode;
@@ -30,10 +30,10 @@ use time::Date;
use url::Url;
pub use crate::nym_api::NymApiClientExt;
use nym_mixnet_contract_common::NymNodeDetails;
pub use nym_mixnet_contract_common::{
mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId,
};
// re-export the type to not break existing imports
pub use crate::coconut::EcashApiClient;
@@ -106,7 +106,9 @@ impl Config {
pub struct Client<C, S = NoSigner> {
// ideally they would have been read-only, but unfortunately rust doesn't have such features
// #[deprecated(note = "please use `nym_api_client` instead")]
pub nym_api: nym_api::Client,
// pub nym_api_client: NymApiClient,
pub nyxd: NyxdClient<C, S>,
}
@@ -243,6 +245,50 @@ impl<C, S> Client<C, S> {
Ok(self.nym_api.get_gateways().await?)
}
// TODO: combine with NymApiClient...
pub async fn get_all_cached_described_nodes(
&self,
) -> Result<Vec<NymNodeDescription>, 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 descriptions = Vec::new();
loop {
let mut res = self.nym_api.get_nodes_described(Some(page), None).await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
// TODO: combine with NymApiClient...
pub async fn get_all_cached_bonded_nym_nodes(
&self,
) -> Result<Vec<NymNodeDetails>, 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 bonds = Vec::new();
loop {
let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?;
bonds.append(&mut res.data);
if bonds.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(bonds)
}
pub async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
@@ -283,7 +329,7 @@ impl NymApiClient {
self.nym_api.change_base_url(new_endpoint);
}
#[deprecated(note = "use get_basic_active_mixing_assigned_nodes instead")]
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")]
pub async fn get_basic_mixnodes(
&self,
semver_compatibility: Option<String>,
@@ -320,7 +366,7 @@ impl NymApiClient {
loop {
let mut res = self
.nym_api
.get_all_basic_entry_assigned_nodes(
.get_basic_entry_assigned_nodes(
semver_compatibility.clone(),
false,
Some(page),
@@ -341,7 +387,7 @@ impl NymApiClient {
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
pub async fn get_basic_active_mixing_assigned_nodes(
pub async fn get_all_basic_active_mixing_assigned_nodes(
&self,
semver_compatibility: Option<String>,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
@@ -371,6 +417,32 @@ impl NymApiClient {
Ok(nodes)
}
/// retrieve basic information for all bonded nodes on the network
pub async fn get_all_basic_nodes(
&self,
semver_compatibility: Option<String>,
) -> 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();
loop {
let mut res = self
.nym_api
.get_basic_nodes(semver_compatibility.clone(), false, Some(page), None)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
}
pub async fn get_cached_active_mixnodes(
&self,
) -> Result<Vec<MixNodeDetails>, ValidatorClientError> {
@@ -397,6 +469,48 @@ impl NymApiClient {
Ok(self.nym_api.get_gateways_described().await?)
}
pub async fn get_all_described_nodes(
&self,
) -> Result<Vec<NymNodeDescription>, 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 descriptions = Vec::new();
loop {
let mut res = self.nym_api.get_nodes_described(Some(page), None).await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
pub async fn get_all_bonded_nym_nodes(
&self,
) -> Result<Vec<NymNodeDetails>, 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 bonds = Vec::new();
loop {
let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?;
bonds.append(&mut res.data);
if bonds.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(bonds)
}
pub async fn get_gateway_core_status_count(
&self,
identity: IdentityKeyRef<'_>,
@@ -11,9 +11,10 @@ use nym_api_requests::ecash::models::{
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse,
AnnotationResponse, LegacyDescribedMixNode, NodePerformanceResponse, NymNodeDescription,
};
use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse;
use nym_api_requests::pagination::PaginatedResponse;
pub use nym_api_requests::{
ecash::{
models::{
@@ -38,7 +39,7 @@ use nym_contracts_common::IdentityKey;
pub use nym_http_api_client::Client;
use nym_http_api_client::{ApiClient, NO_PARAMS};
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId};
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId, NymNodeDetails};
use time::format_description::BorrowedFormatItem;
use time::Date;
@@ -119,6 +120,44 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_nodes_described(
&self,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedResponse<NymNodeDescription>, NymAPIError> {
let mut params = Vec::new();
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()))
}
self.get_json(&[routes::API_VERSION, "nym-nodes", "described"], &params)
.await
}
async fn get_nym_nodes(
&self,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedResponse<NymNodeDetails>, NymAPIError> {
let mut params = Vec::new();
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()))
}
self.get_json(&[routes::API_VERSION, "nym-nodes", "bonded"], &params)
.await
}
async fn get_basic_mixnodes(
&self,
semver_compatibility: Option<String>,
@@ -167,7 +206,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
async fn get_all_basic_entry_assigned_nodes(
async fn get_basic_entry_assigned_nodes(
&self,
semver_compatibility: Option<String>,
no_legacy: bool,
@@ -247,6 +286,38 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_basic_nodes(
&self,
semver_compatibility: Option<String>,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if let Some(arg) = &semver_compatibility {
params.push(("semver_compatibility", arg.clone()))
}
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()))
}
self.get_json(
&[routes::API_VERSION, "unstable", "nym-nodes", "skimmed"],
&params,
)
.await
}
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE],
@@ -17,6 +17,7 @@ use crate::{
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
/// Full details associated with given mixnode.
@@ -647,14 +648,39 @@ impl From<LegacyMixLayer> for u8 {
export_to = "ts-packages/types/src/types/rust/PendingMixnodeChanges.ts"
)
)]
#[cw_serde]
#[derive(Default, Copy)]
// note: we had to remove `#[cw_serde]` as it enforces `#[serde(deny_unknown_fields)]` which we do not want
// with the addition of .cost_params_change field
#[derive(
::cosmwasm_schema::serde::Serialize,
::cosmwasm_schema::serde::Deserialize,
::std::clone::Clone,
::std::fmt::Debug,
::std::cmp::PartialEq,
::cosmwasm_schema::schemars::JsonSchema,
Default,
Copy,
)]
#[schemars(crate = "::cosmwasm_schema::schemars")]
pub struct PendingMixNodeChanges {
pub pledge_change: Option<EpochEventId>,
#[serde(default)]
pub cost_params_change: Option<IntervalEventId>,
}
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct LegacyPendingMixNodeChanges {
pub pledge_change: Option<EpochEventId>,
}
impl From<PendingMixNodeChanges> for LegacyPendingMixNodeChanges {
fn from(value: PendingMixNodeChanges) -> Self {
LegacyPendingMixNodeChanges {
pledge_change: value.pledge_change,
}
}
}
impl PendingMixNodeChanges {
pub fn new_empty() -> PendingMixNodeChanges {
PendingMixNodeChanges {
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Display, Formatter};
use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -112,12 +112,18 @@ impl PemStorableKeyPair for KeyPair {
}
}
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
#[derive(PartialEq, Eq, Hash, Copy, Clone)]
pub struct PublicKey(x25519_dalek::PublicKey);
impl Display for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_base58_string())
Display::fmt(&self.to_base58_string(), f)
}
}
impl Debug for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.to_base58_string(), f)
}
}
@@ -31,8 +31,16 @@ pub mod option_bs58_x25519_pubkey {
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<PublicKey>, D::Error> {
let s = Option::<String>::deserialize(deserializer)?;
s.map(|s| PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom))
.transpose()
match Option::<String>::deserialize(deserializer)? {
None => Ok(None),
Some(s) => {
if s.is_empty() {
Ok(None)
} else {
Some(PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom))
.transpose()
}
}
}
}
}
+9 -3
View File
@@ -5,7 +5,7 @@ pub use ed25519_dalek::SignatureError;
use ed25519_dalek::{Signer, SigningKey};
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Display, Formatter};
use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -119,12 +119,18 @@ impl PemStorableKeyPair for KeyPair {
}
/// ed25519 EdDSA Public Key
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct PublicKey(ed25519_dalek::VerifyingKey);
impl Display for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_base58_string())
Display::fmt(&self.to_base58_string(), f)
}
}
impl Debug for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.to_base58_string(), f)
}
}
+8 -1
View File
@@ -35,6 +35,9 @@ pub enum HttpClientError<E: Display = String> {
source: reqwest::Error,
},
#[error("failed to deserialise received response: {source}")]
ResponseDeserialisationFailure { source: serde_json::Error },
#[error("provided url is malformed: {source}")]
MalformedUrl {
#[from]
@@ -526,7 +529,11 @@ where
}
if res.status().is_success() {
Ok(res.json().await?)
let text = res.text().await?;
match serde_json::from_str(&text) {
Ok(res) => Ok(res),
Err(source) => Err(HttpClientError::ResponseDeserialisationFailure { source }),
}
} else if res.status() == StatusCode::NOT_FOUND {
Err(HttpClientError::NotFound)
} else {
+1
View File
@@ -11,6 +11,7 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum-client-ip.workspace = true
axum.workspace = true
bytes = { workspace = true }
colored.workspace = true
+3 -3
View File
@@ -1,18 +1,18 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use axum::extract::{ConnectInfo, Request};
use axum::extract::Request;
use axum::http::header::{HOST, USER_AGENT};
use axum::http::HeaderValue;
use axum::middleware::Next;
use axum::response::IntoResponse;
use axum_client_ip::InsecureClientIp;
use colored::Colorize;
use std::net::SocketAddr;
use std::time::Instant;
use tracing::info;
pub async fn logger(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
InsecureClientIp(addr): InsecureClientIp,
request: Request,
next: Next,
) -> impl IntoResponse {
+4
View File
@@ -286,6 +286,10 @@ impl NymTopology {
self.get_gateway(gateway_identity).is_some()
}
pub fn insert_gateway(&mut self, gateway: gateway::LegacyNode) {
self.gateways.push(gateway)
}
pub fn set_gateways(&mut self, gateways: Vec<gateway::LegacyNode>) {
self.gateways = gateways
}
+1 -1
View File
@@ -116,7 +116,7 @@ impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode {
});
}
let layer = match value.epoch_role {
let layer = match value.role {
NodeRole::Mixnode { layer } => layer
.try_into()
.map_err(|_| MixnodeConversionError::InvalidLayer)?,
+1 -1
View File
@@ -68,7 +68,7 @@ pub async fn current_network_topology_async(
let api_client = NymApiClient::new(url);
let mixnodes = api_client
.get_basic_active_mixing_assigned_nodes(None)
.get_all_basic_active_mixing_assigned_nodes(None)
.await?;
let gateways = api_client.get_all_basic_entry_assigned_nodes(None).await?;
+5 -2
View File
@@ -158,10 +158,13 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
.ok_or(Error::MissingClientBandwidthEntry)?
.client_id
{
storage.create_bandwidth_entry(client_id).await?;
let bandwidth = storage
.get_available_bandwidth(client_id)
.await?
.ok_or(Error::MissingClientBandwidthEntry)?;
Ok(Some(BandwidthStorageManager::new(
storage,
ClientBandwidth::new(Default::default()),
ClientBandwidth::new(bandwidth.into()),
client_id,
BandwidthFlushingBehaviourConfig::default(),
true,
+7 -6
View File
@@ -84,12 +84,13 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
.ok_or(Error::InconsistentConsumedBytes)?
.try_into()
.map_err(|_| Error::InconsistentConsumedBytes)?;
if bandwidth_manager
.write()
.await
.try_use_bandwidth(spent_bandwidth)
.await
.is_err()
if spent_bandwidth > 0
&& bandwidth_manager
.write()
.await
.try_use_bandwidth(spent_bandwidth)
.await
.is_err()
{
let success = self.remove_peer().await?;
return Ok(!success);
@@ -3689,8 +3689,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -5239,8 +5238,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -5575,8 +5573,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -7595,8 +7592,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -315,8 +315,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -323,8 +323,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -317,8 +317,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -319,8 +319,7 @@
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"Percent": {
"description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)",
@@ -4,6 +4,7 @@
use super::helpers::must_get_gateway_bond_by_owner;
use super::storage;
use crate::constants::default_node_costs;
use crate::interval::storage as interval_storage;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::nodes::helpers::save_new_nymnode_with_id;
use crate::nodes::transactions::add_nym_node_inner;
@@ -115,6 +116,10 @@ pub fn try_migrate_to_nymnode(
comment: "legacy gateway did not have a pre-assigned node id".to_string(),
})?;
let current_epoch =
interval_storage::current_interval(deps.storage)?.current_epoch_absolute_id();
let previous_epoch = current_epoch.saturating_sub(1);
// create nym-node entry
// for gateways it's quite straightforward as there are no delegations or rewards to worry about
save_new_nymnode_with_id(
@@ -125,6 +130,7 @@ pub fn try_migrate_to_nymnode(
cost_params,
info.sender.clone(),
gateway_bond.pledge_amount,
previous_epoch,
)?;
storage::PREASSIGNED_LEGACY_IDS.remove(deps.storage, gateway_identity.clone());
+6 -3
View File
@@ -22,6 +22,8 @@ pub(crate) fn save_new_nymnode(
pledge: Coin,
) -> Result<NodeId, MixnetContractError> {
let node_id = next_nymnode_id_counter(storage)?;
let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id();
save_new_nymnode_with_id(
storage,
node_id,
@@ -30,11 +32,13 @@ pub(crate) fn save_new_nymnode(
cost_params,
owner,
pledge,
current_epoch,
)?;
Ok(node_id)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn save_new_nymnode_with_id(
storage: &mut dyn Storage,
node_id: NodeId,
@@ -43,10 +47,9 @@ pub(crate) fn save_new_nymnode_with_id(
cost_params: NodeCostParams,
owner: Addr,
pledge: Coin,
last_rewarding_epoch: u32,
) -> Result<(), MixnetContractError> {
let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id();
let node_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?;
let node_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, last_rewarding_epoch)?;
let node_bond = NymNodeBond::new(node_id, owner, pledge, node, bonding_height);
// save node bond data
+32 -2
View File
@@ -52,8 +52,8 @@ pub(crate) fn save_assignment(
// update metadata
let mut metadata = ROLES_METADATA.load(storage, inactive)?;
let last = assignment.nodes.last().copied().unwrap_or_default();
metadata.set_highest_id(last, assignment.role);
let highest_id = assignment.nodes.iter().max().copied().unwrap_or_default();
metadata.set_highest_id(highest_id, assignment.role);
metadata.set_role_count(assignment.role, assignment.nodes.len() as u32);
if assignment.is_final_assignment() {
metadata.fully_assigned = true
@@ -140,6 +140,7 @@ pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), Mixnet
mod tests {
use super::*;
use crate::support::tests::test_helpers;
use crate::support::tests::test_helpers::TestSetup;
#[test]
fn next_id() {
@@ -149,4 +150,33 @@ mod tests {
assert_eq!(i, next_nymnode_id_counter(deps.as_mut().storage).unwrap());
}
}
#[test]
fn assigning_role_uses_highest_id_even_if_not_sorted() {
let mut test = TestSetup::new();
let deps = test.deps_mut();
let sorted = RoleAssignment {
role: Role::EntryGateway,
nodes: vec![1, 2, 3],
};
let unsorted = RoleAssignment {
role: Role::Layer1,
nodes: vec![8, 5, 4],
};
save_assignment(deps.storage, sorted).unwrap();
save_assignment(deps.storage, unsorted).unwrap();
let storage = deps.as_ref().storage;
let active_bucket = ACTIVE_ROLES_BUCKET.load(storage).unwrap();
let inactive = active_bucket.other() as u8;
let metadata = ROLES_METADATA.load(storage, inactive).unwrap();
assert_eq!(metadata.entry_gateway_metadata.highest_id, 3);
assert_eq!(metadata.layer1_metadata.highest_id, 8);
assert_eq!(metadata.highest_rewarded_id(), 8)
}
}
+15
View File
@@ -12,6 +12,8 @@ pub use nym_explorer_api_requests::{
// Paths
const API_VERSION: &str = "v1";
const TMP: &str = "tmp";
const UNSTABLE: &str = "unstable";
const MIXNODES: &str = "mix-nodes";
const GATEWAYS: &str = "gateways";
@@ -53,6 +55,12 @@ impl ExplorerClient {
Ok(Self { client, url })
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new_with_timeout(url: url::Url, timeout: Duration) -> Result<Self, ExplorerApiError> {
let client = reqwest::Client::builder().timeout(timeout).build()?;
Ok(Self { client, url })
}
async fn send_get_request(
&self,
paths: &[&str],
@@ -86,6 +94,13 @@ impl ExplorerClient {
pub async fn get_gateways(&self) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
self.query_explorer_api(&[API_VERSION, GATEWAYS]).await
}
pub async fn unstable_get_gateways(
&self,
) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
self.query_explorer_api(&[API_VERSION, TMP, UNSTABLE, GATEWAYS])
.await
}
}
fn combine_url(mut base_url: Url, paths: &[&str]) -> Result<Url, ExplorerApiError> {
@@ -4,6 +4,7 @@
use crate::state::ExplorerApiStateContext;
use log::{info, warn};
use nym_explorer_api_requests::Location;
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use nym_task::TaskClient;
pub(crate) struct GeoLocateTask {
@@ -25,6 +26,7 @@ impl GeoLocateTask {
_ = interval_timer.tick() => {
self.locate_mix_nodes().await;
self.locate_gateways().await;
self.locate_nym_nodes().await;
}
_ = self.shutdown.recv() => {
trace!("Listener: Received shutdown");
@@ -109,6 +111,83 @@ impl GeoLocateTask {
trace!("All mix nodes located");
}
async fn locate_nym_nodes(&mut self) {
// I'm unwrapping to the default value to get rid of an extra indentation level from the `if let Some(...) = ...`
// If the value is None, we'll unwrap to an empty hashmap and the `values()` loop won't do any work anyway
let nym_nodes = self.state.inner.nymnodes.get_bonded_nymnodes().await;
let geo_ip = self.state.inner.geo_ip.0.clone();
for (i, cache_item) in nym_nodes.values().enumerate() {
if self
.state
.inner
.nymnodes
.is_location_valid(cache_item.node_id())
.await
{
// when the cached location is valid, don't locate and continue to next mix node
continue;
}
let bonded_host = &cache_item.bond_information.node.host;
match geo_ip.query(
bonded_host,
Some(
cache_item
.bond_information
.node
.custom_http_port
.unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT),
),
) {
Ok(opt) => match opt {
Some(location) => {
let location: Location = location.into();
trace!(
"{} mix nodes already located. host {} is located in {:#?}",
i,
bonded_host,
location.three_letter_iso_country_code,
);
if i > 0 && (i % 100) == 0 {
info!("Located {} nym-nodes...", i + 1,);
}
self.state
.inner
.nymnodes
.set_location(cache_item.node_id(), Some(location))
.await;
// one node has been located, so return out of the loop
return;
}
None => {
warn!("❌ Location for {bonded_host} not found.");
self.state
.inner
.nymnodes
.set_location(cache_item.node_id(), None)
.await;
}
},
Err(_e) => {
// warn!(
// "❌ Oh no! Location for {} failed. Error: {:#?}",
// cache_item.mix_node().host,
// e
// );
}
};
}
trace!("All nym-nodes nodes located");
}
async fn locate_gateways(&mut self) {
let gateways = self.state.inner.gateways.get_gateways().await;
+2
View File
@@ -10,6 +10,7 @@ use crate::gateways::http::gateways_make_default_routes;
use crate::http::swagger::get_docs;
use crate::mix_node::http::mix_node_make_default_routes;
use crate::mix_nodes::http::mix_nodes_make_default_routes;
use crate::nym_nodes::http::unstable_temp_nymnodes_make_default_routes;
use crate::overview::http::overview_make_default_routes;
use crate::ping::http::ping_make_default_routes;
use crate::service_providers::http::service_providers_make_default_routes;
@@ -58,6 +59,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket<Build> {
"/ping" => ping_make_default_routes(&openapi_settings),
"/validators" => validators_make_default_routes(&openapi_settings),
"/service-providers" => service_providers_make_default_routes(&openapi_settings),
"/tmp/unstable" => unstable_temp_nymnodes_make_default_routes(&openapi_settings),
};
building_rocket
+1
View File
@@ -22,6 +22,7 @@ mod http;
mod location;
mod mix_node;
pub(crate) mod mix_nodes;
mod nym_nodes;
mod overview;
mod ping;
pub(crate) mod service_providers;
+26
View File
@@ -0,0 +1,26 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::state::ExplorerApiStateContext;
use nym_explorer_api_requests::PrettyDetailedGatewayBond;
use okapi::openapi3::OpenApi;
use rocket::serde::json::Json;
use rocket::{Route, State};
use rocket_okapi::settings::OpenApiSettings;
pub fn unstable_temp_nymnodes_make_default_routes(
settings: &OpenApiSettings,
) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![settings: all_gateways]
}
#[openapi(tag = "UNSTABLE")]
#[get("/gateways")]
pub(crate) async fn all_gateways(
state: &State<ExplorerApiStateContext>,
) -> Json<Vec<PrettyDetailedGatewayBond>> {
let mut gateways = state.inner.gateways.get_detailed_gateways().await;
gateways.append(&mut state.inner.nymnodes.pretty_gateways().await);
Json(gateways)
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_mixnet_contract_common::NodeId;
use crate::location::LocationCache;
pub(crate) type NymNodeLocationCache = LocationCache<NodeId>;
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::time::Duration;
pub(crate) mod http;
pub(crate) mod location;
pub(crate) mod models;
pub(crate) const CACHE_ENTRY_TTL: Duration = Duration::from_secs(1200);
+154
View File
@@ -0,0 +1,154 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::location::{LocationCache, LocationCacheItem};
use crate::nym_nodes::location::NymNodeLocationCache;
use crate::nym_nodes::CACHE_ENTRY_TTL;
use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond};
use nym_mixnet_contract_common::{Gateway, NodeId, NymNodeDetails};
use nym_validator_client::models::NymNodeDescription;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::{RwLock, RwLockReadGuard};
pub(crate) struct NymNodesCache {
pub(crate) valid_until: SystemTime,
pub(crate) bonded_nym_nodes: HashMap<NodeId, NymNodeDetails>,
pub(crate) described_nodes: HashMap<NodeId, NymNodeDescription>,
}
impl NymNodesCache {
fn new() -> Self {
NymNodesCache {
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
bonded_nym_nodes: Default::default(),
described_nodes: Default::default(),
}
}
// fn is_valid(&self) -> bool {
// self.valid_until >= SystemTime::now()
// }
}
#[derive(Clone)]
pub(crate) struct ThreadSafeNymNodesCache {
nymnodes: Arc<RwLock<NymNodesCache>>,
locations: Arc<RwLock<LocationCache<NodeId>>>,
}
impl ThreadSafeNymNodesCache {
pub(crate) fn new() -> Self {
ThreadSafeNymNodesCache {
nymnodes: Arc::new(RwLock::new(NymNodesCache::new())),
locations: Arc::new(RwLock::new(NymNodeLocationCache::new())),
}
}
pub(crate) fn new_with_location_cache(locations: NymNodeLocationCache) -> Self {
ThreadSafeNymNodesCache {
nymnodes: Arc::new(RwLock::new(NymNodesCache::new())),
locations: Arc::new(RwLock::new(locations)),
}
}
pub(crate) async fn is_location_valid(&self, node_id: NodeId) -> bool {
self.locations
.read()
.await
.get(&node_id)
.map_or(false, |cache_item| {
cache_item.valid_until > SystemTime::now()
})
}
pub(crate) async fn get_bonded_nymnodes(
&self,
) -> RwLockReadGuard<HashMap<NodeId, NymNodeDetails>> {
let guard = self.nymnodes.read().await;
RwLockReadGuard::map(guard, |n| &n.bonded_nym_nodes)
}
pub(crate) async fn get_locations(&self) -> NymNodeLocationCache {
self.locations.read().await.clone()
}
pub(crate) async fn set_location(&self, node_id: NodeId, location: Option<Location>) {
// cache the location for this mix node so that it can be used when the mix node list is refreshed
self.locations
.write()
.await
.insert(node_id, LocationCacheItem::new_from_location(location));
}
pub(crate) async fn update_cache(
&self,
all_bonds: Vec<NymNodeDetails>,
descriptions: Vec<NymNodeDescription>,
) {
let mut guard = self.nymnodes.write().await;
guard.bonded_nym_nodes = all_bonds
.into_iter()
.map(|details| (details.node_id(), details))
.collect();
guard.described_nodes = descriptions
.into_iter()
.map(|description| (description.node_id, description))
.collect();
guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL;
}
pub(crate) async fn pretty_gateways(&self) -> Vec<PrettyDetailedGatewayBond> {
let nodes_guard = self.nymnodes.read().await;
let location_guard = self.locations.read().await;
let mut pretty_gateways = vec![];
for (node_id, native_nymnode) in &nodes_guard.bonded_nym_nodes {
let Some(description) = nodes_guard.described_nodes.get(node_id) else {
continue;
};
if description.description.declared_role.entry {
let location = location_guard.get(node_id);
let bond = &native_nymnode.bond_information;
pretty_gateways.push(PrettyDetailedGatewayBond {
pledge_amount: bond.original_pledge.clone(),
owner: bond.owner.clone(),
block_height: bond.bonding_height,
gateway: Gateway {
host: bond.node.host.clone(),
mix_port: description.description.mix_port(),
clients_port: description.description.mixnet_websockets.ws_port,
location: description
.description
.auxiliary_details
.location
.as_ref()
.map(|l| l.to_string())
.unwrap_or_default(),
sphinx_key: description
.description
.host_information
.keys
.x25519
.to_base58_string(),
identity_key: bond.node.identity_key.clone(),
version: description
.description
.build_information
.build_version
.clone(),
},
proxy: None,
location: location.and_then(|l| l.location.clone()),
})
}
}
pretty_gateways
}
}
+9
View File
@@ -18,6 +18,8 @@ use crate::gateways::models::ThreadsafeGatewayCache;
use crate::mix_node::models::ThreadsafeMixNodeCache;
use crate::mix_nodes::location::MixnodeLocationCache;
use crate::mix_nodes::models::ThreadsafeMixNodesCache;
use crate::nym_nodes::location::NymNodeLocationCache;
use crate::nym_nodes::models::ThreadSafeNymNodesCache;
use crate::ping::models::ThreadsafePingCache;
use crate::validators::models::ThreadsafeValidatorCache;
@@ -30,6 +32,7 @@ pub struct ExplorerApiState {
pub(crate) gateways: ThreadsafeGatewayCache,
pub(crate) mixnode: ThreadsafeMixNodeCache,
pub(crate) mixnodes: ThreadsafeMixNodesCache,
pub(crate) nymnodes: ThreadSafeNymNodesCache,
pub(crate) ping: ThreadsafePingCache,
pub(crate) validators: ThreadsafeValidatorCache,
pub(crate) geo_ip: ThreadsafeGeoIp,
@@ -49,6 +52,7 @@ pub struct ExplorerApiStateOnDisk {
pub(crate) country_node_distribution: CountryNodesDistribution,
pub(crate) mixnode_location_cache: MixnodeLocationCache,
pub(crate) gateway_location_cache: GatewayLocationCache,
pub(crate) nymnode_location_cache: NymNodeLocationCache,
pub(crate) as_at: DateTime<Utc>,
}
@@ -85,6 +89,9 @@ impl ExplorerApiStateContext {
mixnodes: ThreadsafeMixNodesCache::new_with_location_cache(
state.mixnode_location_cache,
),
nymnodes: ThreadSafeNymNodesCache::new_with_location_cache(
state.nymnode_location_cache,
),
ping: ThreadsafePingCache::new(),
validators: ThreadsafeValidatorCache::new(),
validator_client: ThreadsafeValidatorClient::new(),
@@ -101,6 +108,7 @@ impl ExplorerApiStateContext {
gateways: ThreadsafeGatewayCache::new(),
mixnode: ThreadsafeMixNodeCache::new(),
mixnodes: ThreadsafeMixNodesCache::new(),
nymnodes: ThreadSafeNymNodesCache::new(),
ping: ThreadsafePingCache::new(),
validators: ThreadsafeValidatorCache::new(),
validator_client: ThreadsafeValidatorClient::new(),
@@ -117,6 +125,7 @@ impl ExplorerApiStateContext {
country_node_distribution: self.inner.country_node_distribution.get_all().await,
mixnode_location_cache: self.inner.mixnodes.get_locations().await,
gateway_location_cache: self.inner.gateways.get_locations().await,
nymnode_location_cache: self.inner.nymnodes.get_locations().await,
as_at: Utc::now(),
};
serde_json::to_writer(file, &state).expect("error writing state to disk");
+54 -5
View File
@@ -1,16 +1,16 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_mixnet_contract_common::GatewayBond;
use crate::mix_nodes::CACHE_REFRESH_RATE;
use crate::state::ExplorerApiStateContext;
use nym_mixnet_contract_common::{GatewayBond, NymNodeDetails};
use nym_task::TaskClient;
use nym_validator_client::models::MixNodeBondAnnotated;
use nym_validator_client::models::{MixNodeBondAnnotated, NymNodeDescription};
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse};
use nym_validator_client::{QueryHttpRpcValidatorClient, ValidatorClientError};
use std::future::Future;
use crate::mix_nodes::CACHE_REFRESH_RATE;
use crate::state::ExplorerApiStateContext;
use tokio::time::MissedTickBehavior;
pub(crate) struct ExplorerApiTasks {
state: ExplorerApiStateContext,
@@ -39,6 +39,28 @@ impl ExplorerApiTasks {
bonds
}
async fn retrieve_bonded_nymnodes(&self) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
info!("About to retrieve all nymnode bonds...");
self.state
.inner
.validator_client
.0
.get_all_cached_bonded_nym_nodes()
.await
}
async fn retrieve_node_descriptions(
&self,
) -> Result<Vec<NymNodeDescription>, ValidatorClientError> {
info!("About to retrieve node descriptions...");
self.state
.inner
.validator_client
.0
.get_all_cached_described_nodes()
.await
}
async fn retrieve_all_mixnodes(&self) -> Vec<MixNodeBondAnnotated> {
info!("About to retrieve all mixnode bonds...");
self.retrieve_mixnodes(
@@ -130,10 +152,33 @@ impl ExplorerApiTasks {
}
}
async fn update_nymnodes_cache(&self) {
let nym_node_bonds = self.retrieve_bonded_nymnodes().await.unwrap_or_else(|err| {
error!("failed to retrieve nym node bonds: {err}");
Vec::new()
});
let all_descriptions = self
.retrieve_node_descriptions()
.await
.unwrap_or_else(|err| {
error!("failed to retrieve node descriptions: {err}");
Vec::new()
});
self.state
.inner
.nymnodes
.update_cache(nym_node_bonds, all_descriptions)
.await
}
pub(crate) fn start(mut self) {
info!("Spawning mix nodes task runner...");
tokio::spawn(async move {
let mut interval_timer = tokio::time::interval(CACHE_REFRESH_RATE);
interval_timer.set_missed_tick_behavior(MissedTickBehavior::Skip);
while !self.shutdown.is_shutdown() {
tokio::select! {
_ = interval_timer.tick() => {
@@ -147,6 +192,10 @@ impl ExplorerApiTasks {
info!("Updating mix node cache...");
self.update_mixnode_cache().await;
info!("Updating nymnode cache...");
self.update_nymnodes_cache().await;
info!("Done");
}
_ = self.shutdown.recv() => {
trace!("Listener: Received shutdown");
+2
View File
@@ -76,9 +76,11 @@ nym-network-defaults = { path = "../common/network-defaults" }
nym-network-requester = { path = "../service-providers/network-requester" }
nym-node-http-api = { path = "../nym-node/nym-node-http-api" }
nym-pemstore = { path = "../common/pemstore" }
nym-sdk = { path = "../sdk/rust/nym-sdk" }
nym-sphinx = { path = "../common/nymsphinx" }
nym-statistics-common = { path = "../common/statistics" }
nym-task = { path = "../common/task" }
nym-topology = { path = "../common/topology" }
nym-types = { path = "../common/types" }
nym-validator-client = { path = "../common/client-libs/validator-client" }
nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
+60 -2
View File
@@ -3,13 +3,18 @@
use crate::config::Config;
use crate::error::GatewayError;
use async_trait::async_trait;
use nym_crypto::asymmetric::encryption;
use nym_gateway_storage::PersistentStorage;
use nym_pemstore::traits::PemStorableKeyPair;
use nym_pemstore::KeyPairPath;
use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent};
use nym_topology::{gateway, NymTopology, TopologyProvider};
use std::path::Path;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::debug;
use url::Url;
pub async fn load_network_requester_config<P: AsRef<Path>>(
id: &str,
@@ -93,3 +98,56 @@ pub(crate) fn load_sphinx_keys(config: &Config) -> Result<encryption::KeyPair, G
);
load_keypair(sphinx_paths, "gateway sphinx")
}
#[derive(Clone)]
pub struct GatewayTopologyProvider {
inner: Arc<Mutex<GatewayTopologyProviderInner>>,
}
impl GatewayTopologyProvider {
pub fn new(
gateway_node: gateway::LegacyNode,
user_agent: UserAgent,
nym_api_url: Vec<Url>,
) -> GatewayTopologyProvider {
GatewayTopologyProvider {
inner: Arc::new(Mutex::new(GatewayTopologyProviderInner {
inner: NymApiTopologyProvider::new(
NymApiTopologyProviderConfig {
min_mixnode_performance: 50,
min_gateway_performance: 0,
},
nym_api_url,
env!("CARGO_PKG_VERSION").to_string(),
Some(user_agent),
),
gateway_node,
})),
}
}
}
struct GatewayTopologyProviderInner {
inner: NymApiTopologyProvider,
gateway_node: gateway::LegacyNode,
}
#[async_trait]
impl TopologyProvider for GatewayTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
let mut guard = self.inner.lock().await;
match guard.inner.get_new_topology().await {
None => None,
Some(mut base) => {
if !base.gateway_exists(&guard.gateway_node.identity_key) {
debug!(
"{} didn't exist in topology. inserting it.",
guard.gateway_node.identity_key
);
base.insert_gateway(guard.gateway_node.clone());
}
Some(base)
}
}
}
}
+52 -2
View File
@@ -12,9 +12,12 @@ use crate::http::HttpApiBuilder;
use crate::node::client_handling::active_clients::ActiveClientsStore;
use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, MessageRouter};
use crate::node::client_handling::websocket;
use crate::node::helpers::{initialise_main_storage, load_network_requester_config};
use crate::node::helpers::{
initialise_main_storage, load_network_requester_config, GatewayTopologyProvider,
};
use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler;
use futures::channel::{mpsc, oneshot};
use nym_bin_common::bin_info;
use nym_credential_verification::ecash::{
credential_sender::CredentialHandlerConfig, EcashManager,
};
@@ -25,13 +28,15 @@ use nym_network_requester::{LocalGateway, NRServiceProviderBuilder, RequestFilte
use nym_node_http_api::state::metrics::SharedSessionStats;
use nym_statistics_common::events::{self, StatsEventSender};
use nym_task::{TaskClient, TaskHandle, TaskManager};
use nym_topology::NetworkAddress;
use nym_types::gateway::GatewayNodeDetailsResponse;
use nym_validator_client::client::NodeId;
use nym_validator_client::nyxd::{Coin, CosmWasmClient};
use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient};
use rand::seq::SliceRandom;
use rand::thread_rng;
use statistics::GatewayStatisticsCollector;
use std::net::SocketAddr;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::*;
@@ -225,6 +230,39 @@ impl<St> Gateway<St> {
crate::helpers::node_details(&self.config).await
}
fn gateway_topology_provider(&self) -> GatewayTopologyProvider {
GatewayTopologyProvider::new(
self.as_topology_node(),
bin_info!().into(),
self.config.gateway.nym_api_urls.clone(),
)
}
fn as_topology_node(&self) -> nym_topology::gateway::LegacyNode {
let ip = self
.config
.host
.public_ips
.first()
.copied()
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
let mix_host = SocketAddr::new(ip, self.config.gateway.mix_port);
nym_topology::gateway::LegacyNode {
// those fields are irrelevant for the purposes of routing so it's fine if they're inaccurate.
// the only thing that matters is the identity key (and maybe version)
node_id: NodeId::MAX,
mix_host,
host: NetworkAddress::IpAddr(ip),
clients_ws_port: self.config.gateway.clients_port,
clients_wss_port: self.config.gateway.clients_wss_port,
sphinx_key: *self.sphinx_keypair.public_key(),
identity_key: *self.identity_keypair.public_key(),
version: env!("CARGO_PKG_VERSION").into(),
}
}
fn start_mix_socket_listener(
&self,
ack_sender: MixForwardingSender,
@@ -257,6 +295,7 @@ impl<St> Gateway<St> {
async fn start_authenticator(
&mut self,
forwarding_channel: MixForwardingSender,
topology_provider: GatewayTopologyProvider,
shutdown: TaskClient,
ecash_verifier: Arc<EcashManager<St>>,
) -> Result<StartedAuthenticator, Box<dyn std::error::Error + Send + Sync>>
@@ -304,6 +343,7 @@ impl<St> Gateway<St> {
.with_shutdown(shutdown.fork("authenticator"))
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(Box::new(topology_provider))
.with_on_start(on_start_tx);
if let Some(custom_mixnet) = &opts.custom_mixnet_path {
@@ -352,6 +392,7 @@ impl<St> Gateway<St> {
async fn start_authenticator(
&self,
_forwarding_channel: MixForwardingSender,
_topology_provider: GatewayTopologyProvider,
_shutdown: TaskClient,
_ecash_verifier: Arc<EcashManager<St>>,
) -> Result<StartedAuthenticator, Box<dyn std::error::Error + Send + Sync>> {
@@ -424,6 +465,7 @@ impl<St> Gateway<St> {
async fn start_network_requester(
&self,
forwarding_channel: MixForwardingSender,
topology_provider: GatewayTopologyProvider,
shutdown: TaskClient,
) -> Result<StartedNetworkRequester, GatewayError> {
info!("Starting network requester...");
@@ -451,6 +493,7 @@ impl<St> Gateway<St> {
.with_custom_gateway_transceiver(Box::new(transceiver))
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(Box::new(topology_provider))
.with_on_start(on_start_tx);
if let Some(custom_mixnet) = &nr_opts.custom_mixnet_path {
@@ -488,6 +531,7 @@ impl<St> Gateway<St> {
async fn start_ip_packet_router(
&self,
forwarding_channel: MixForwardingSender,
topology_provider: GatewayTopologyProvider,
shutdown: TaskClient,
) -> Result<LocalEmbeddedClientHandle, GatewayError> {
info!("Starting IP packet provider...");
@@ -516,6 +560,7 @@ impl<St> Gateway<St> {
.with_custom_gateway_transceiver(Box::new(transceiver))
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(Box::new(topology_provider))
.with_on_start(on_start_tx);
if let Some(custom_mixnet) = &ip_opts.custom_mixnet_path {
@@ -632,6 +677,8 @@ impl<St> Gateway<St> {
shutdown.fork("statistics::GatewayStatisticsCollector"),
);
let topology_provider = self.gateway_topology_provider();
let handler_config = CredentialHandlerConfig {
revocation_bandwidth_penalty: self
.config
@@ -680,6 +727,7 @@ impl<St> Gateway<St> {
let embedded_nr = self
.start_network_requester(
mix_forwarding_channel.clone(),
topology_provider.clone(),
shutdown.fork("NetworkRequester"),
)
.await?;
@@ -695,6 +743,7 @@ impl<St> Gateway<St> {
let embedded_ip_sp = self
.start_ip_packet_router(
mix_forwarding_channel.clone(),
topology_provider.clone(),
shutdown.fork("ip_service_provider"),
)
.await?;
@@ -707,6 +756,7 @@ impl<St> Gateway<St> {
let embedded_auth = self
.start_authenticator(
mix_forwarding_channel,
topology_provider,
shutdown.fork("authenticator"),
ecash_verifier,
)
+3 -15
View File
@@ -2,10 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmwasm_std::Decimal;
use nym_mixnet_contract_common::mixnode::PendingMixNodeChanges;
use nym_mixnet_contract_common::{
GatewayBond, LegacyMixLayer, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding,
};
use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges;
use nym_mixnet_contract_common::{GatewayBond, LegacyMixLayer, MixNodeBond, NodeId, NodeRewarding};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
@@ -64,7 +62,7 @@ pub struct LegacyMixNodeDetailsWithLayer {
/// Adjustments to the mixnode that are ought to happen during future epoch transitions.
#[serde(default)]
pub pending_changes: PendingMixNodeChanges,
pub pending_changes: LegacyPendingMixNodeChanges,
}
impl LegacyMixNodeDetailsWithLayer {
@@ -80,13 +78,3 @@ impl LegacyMixNodeDetailsWithLayer {
self.bond_information.is_unbonding
}
}
impl From<LegacyMixNodeDetailsWithLayer> for MixNodeDetails {
fn from(value: LegacyMixNodeDetailsWithLayer) -> Self {
MixNodeDetails {
bond_information: value.bond_information.into(),
rewarding_details: value.rewarding_details,
pending_changes: value.pending_changes,
}
}
}
+53 -7
View File
@@ -16,7 +16,7 @@ use nym_crypto::asymmetric::x25519::{
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams};
use nym_mixnet_contract_common::rewarding::RewardEstimate;
use nym_mixnet_contract_common::{IdentityKey, Interval, MixNode, NodeId, Percent};
use nym_mixnet_contract_common::{GatewayBond, IdentityKey, Interval, MixNode, NodeId, Percent};
use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
use nym_node_requests::api::v1::authenticator::models::Authenticator;
use nym_node_requests::api::v1::gateway::models::Wireguard;
@@ -138,6 +138,48 @@ pub struct NodePerformance {
pub last_24h: Performance,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(export, export_to = "ts-packages/types/src/types/rust/DisplayRole.ts")
)]
pub enum DisplayRole {
EntryGateway,
Layer1,
Layer2,
Layer3,
ExitGateway,
Standby,
}
impl From<Role> for DisplayRole {
fn from(role: Role) -> Self {
match role {
Role::EntryGateway => DisplayRole::EntryGateway,
Role::Layer1 => DisplayRole::Layer1,
Role::Layer2 => DisplayRole::Layer2,
Role::Layer3 => DisplayRole::Layer3,
Role::ExitGateway => DisplayRole::ExitGateway,
Role::Standby => DisplayRole::Standby,
}
}
}
impl From<DisplayRole> for Role {
fn from(role: DisplayRole) -> Self {
match role {
DisplayRole::EntryGateway => Role::EntryGateway,
DisplayRole::Layer1 => Role::Layer1,
DisplayRole::Layer2 => Role::Layer2,
DisplayRole::Layer3 => Role::Layer3,
DisplayRole::ExitGateway => Role::ExitGateway,
DisplayRole::Standby => Role::Standby,
}
}
}
// imo for now there's no point in exposing more than that,
// nym-api shouldn't be calculating apy or stake saturation for you.
// it should just return its own metrics (performance) and then you can do with it as you wish
@@ -153,7 +195,7 @@ pub struct NodePerformance {
pub struct NodeAnnotation {
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
pub last_24h_performance: Performance,
pub current_role: Option<Role>,
pub current_role: Option<DisplayRole>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
@@ -286,7 +328,7 @@ impl MixNodeBondAnnotated {
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
epoch_role: role,
role,
supported_roles: DeclaredRoles {
mixnode: true,
entry: false,
@@ -345,7 +387,7 @@ impl GatewayBondAnnotated {
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
epoch_role: role,
role,
supported_roles: DeclaredRoles {
mixnode: false,
entry: true,
@@ -810,6 +852,10 @@ impl NymNodeDescription {
}
}
pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
self.description.host_information.keys.ed25519
}
pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode {
let keys = &self.description.host_information.keys;
let entry = if self.description.declared_role.entry {
@@ -827,7 +873,7 @@ impl NymNodeDescription {
// 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.
epoch_role: role,
role,
supported_roles: self.description.declared_role,
entry,
performance,
@@ -935,14 +981,14 @@ impl NymNodeData {
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct LegacyDescribedGateway {
pub bond: LegacyGatewayBondWithId,
pub bond: GatewayBond,
pub self_described: Option<NymNodeData>,
}
impl From<LegacyGatewayBondWithId> for LegacyDescribedGateway {
fn from(bond: LegacyGatewayBondWithId) -> Self {
LegacyDescribedGateway {
bond,
bond: bond.bond,
self_described: None,
}
}
+3 -3
View File
@@ -141,8 +141,8 @@ pub struct SkimmedNode {
#[schemars(with = "String")]
pub x25519_sphinx_pubkey: x25519::PublicKey,
#[serde(alias = "role")]
pub epoch_role: NodeRole,
#[serde(alias = "epoch_role")]
pub role: NodeRole,
// needed for the purposes of sending appropriate test packets
#[serde(default)]
@@ -157,7 +157,7 @@ pub struct SkimmedNode {
impl SkimmedNode {
pub fn get_mix_layer(&self) -> Option<u8> {
match self.epoch_role {
match self.role {
NodeRole::Mixnode { layer } => Some(layer),
_ => None,
}
+6 -6
View File
@@ -24,21 +24,21 @@ use utoipa::IntoParams;
pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
Router::new()
.route(
"/master-verification-key:epoch_id",
"/master-verification-key",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch_id| master_verification_key(epoch_id, ecash_state)
}),
)
.route(
"/aggregated-expiration-date-signatures:expiration_date",
"/aggregated-expiration-date-signatures",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|expiration_date| expiration_date_signatures(expiration_date, ecash_state)
}),
)
.route(
"/aggregated-coin-indices-signatures:epoch_id",
"/aggregated-coin-indices-signatures",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch_id| coin_indices_signatures(epoch_id, ecash_state)
@@ -52,7 +52,7 @@ pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppStat
params(
EpochIdParam
),
path = "/v1/ecash/master-verification-key/{epoch_id}",
path = "/v1/ecash/master-verification-key",
responses(
(status = 200, body = VerificationKeyResponse)
)
@@ -83,7 +83,7 @@ struct ExpirationDateParam {
params(
ExpirationDateParam
),
path = "/v1/ecash/aggregated-expiration-date-signatures/{epoch_id}",
path = "/v1/ecash/aggregated-expiration-date-signatures",
responses(
(status = 200, body = AggregatedExpirationDateSignatureResponse)
)
@@ -120,7 +120,7 @@ async fn expiration_date_signatures(
params(
EpochIdParam
),
path = "/v1/ecash/aggregated-coin-indices-signatures/{epoch_id}",
path = "/v1/ecash/aggregated-coin-indices-signatures",
responses(
(status = 200, body = AggregatedCoinIndicesSignatureResponse)
)
@@ -7,7 +7,7 @@ use crate::ecash::helpers::blind_sign;
use crate::ecash::state::EcashState;
use crate::node_status_api::models::AxumResult;
use crate::support::http::state::AppState;
use axum::extract::Path;
use axum::extract::Query;
use axum::{Json, Router};
use nym_api_requests::ecash::{
BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse,
@@ -32,14 +32,14 @@ pub(crate) fn partial_signing_routes(ecash_state: Arc<EcashState>) -> Router<App
}),
)
.route(
"/partial-expiration-date-signatures:expiration_date",
"/partial-expiration-date-signatures",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state)
}),
)
.route(
"/partial-coin-indices-signatures:epoch_id",
"/partial-coin-indices-signatures",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state)
@@ -127,14 +127,14 @@ struct ExpirationDateParam {
params(
ExpirationDateParam
),
path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}",
path = "/v1/ecash/partial-expiration-date-signatures",
responses(
(status = 200, body = PartialExpirationDateSignatureResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn partial_expiration_date_signatures(
Path(ExpirationDateParam { expiration_date }): Path<ExpirationDateParam>,
Query(ExpirationDateParam { expiration_date }): Query<ExpirationDateParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<PartialExpirationDateSignatureResponse>> {
state.ensure_signer().await?;
@@ -165,14 +165,14 @@ async fn partial_expiration_date_signatures(
params(
EpochIdParam
),
path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}",
path = "/v1/ecash/partial-coin-indices-signatures",
responses(
(status = 200, body = PartialExpirationDateSignatureResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn partial_coin_indices_signatures(
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
Query(EpochIdParam { epoch_id }): Query<EpochIdParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<PartialCoinIndicesSignatureResponse>> {
state.ensure_signer().await?;
+2
View File
@@ -20,6 +20,7 @@ use time::macros::time;
use time::{OffsetDateTime, Time};
use tracing::{error, warn};
#[allow(deprecated)]
pub(crate) fn spending_routes(ecash_state: Arc<EcashState>) -> Router<AppState> {
Router::new()
.route(
@@ -242,6 +243,7 @@ async fn batch_redeem_tickets(
(status = 500, body = ErrorResponse, description = "bloomfilters got disabled"),
)
)]
#[deprecated]
async fn double_spending_filter_v1(
_state: Arc<EcashState>,
) -> AxumResult<Json<SpentCredentialsResponse>> {
@@ -169,7 +169,7 @@ impl EpochAdvancer {
let standby_eligible = all_choices
.iter()
.filter(|node| {
exit_gateways.contains(&node.0.node_id)
!exit_gateways.contains(&node.0.node_id)
&& !entry_gateways.contains(&node.0.node_id)
&& !mixnodes.contains(&node.0.node_id)
})
@@ -228,14 +228,24 @@ impl EpochAdvancer {
)
}
Ok(RewardedSet {
let mut rewarded_set = RewardedSet {
entry_gateways: entry_gateways.into_iter().collect(),
exit_gateways: exit_gateways.into_iter().collect(),
layer1,
layer2,
layer3,
standby,
})
};
// make sure to sort the rewarded set values
rewarded_set.entry_gateways.sort();
rewarded_set.exit_gateways.sort();
rewarded_set.layer1.sort();
rewarded_set.layer2.sort();
rewarded_set.layer3.sort();
rewarded_set.standby.sort();
Ok(rewarded_set)
}
async fn attach_performance_to_eligible_nodes(
+2 -2
View File
@@ -274,8 +274,8 @@ impl<R: MessageReceiver + Send> Monitor<R> {
info!("Received {}/{} packets", total_received, total_sent);
let summary = self.summary_producer.produce_summary(
prepared_packets.tested_mixnodes,
prepared_packets.tested_gateways,
prepared_packets.mixnodes_under_test,
prepared_packets.gateways_under_test,
received,
prepared_packets.invalid_mixnodes,
prepared_packets.invalid_gateways,
+46 -31
View File
@@ -25,7 +25,7 @@ use std::collections::{HashMap, HashSet};
use std::fmt::{self, Display, Formatter};
use std::sync::Arc;
use std::time::Duration;
use tracing::{error, info, trace};
use tracing::{debug, error, info, trace};
const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200);
const DEFAULT_AVERAGE_ACK_DELAY: Duration = Duration::from_millis(200);
@@ -59,10 +59,10 @@ pub(crate) struct PreparedPackets {
pub(super) packets: Vec<GatewayPackets>,
/// Vector containing list of public keys and owners of all nodes mixnodes being tested.
pub(super) tested_mixnodes: Vec<TestableNode>,
pub(super) mixnodes_under_test: Vec<TestableNode>,
/// Vector containing list of public keys and owners of all gateways being tested.
pub(super) tested_gateways: Vec<TestableNode>,
pub(super) gateways_under_test: Vec<TestableNode>,
/// All mixnodes that failed to get parsed correctly or were not version compatible.
/// They will be marked to the validator as being down for the test.
@@ -151,34 +151,38 @@ impl PacketPreparer {
self.contract_cache.wait_for_initial_values().await;
self.described_cache.naive_wait_for_initial_values().await;
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);
loop {
let gateways = self.contract_cache.legacy_gateways_all().await;
let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await;
let nym_nodes = self.contract_cache.nym_nodes().await;
if gateways.len() < minimum_full_routes {
self.topology_wait_backoff(initialisation_backoff).await;
continue;
}
let mut gateways_count = gateways.len();
let mut mixnodes_count = mixnodes.len();
let mut layer1_count = 0;
let mut layer2_count = 0;
let mut layer3_count = 0;
for mix in mixnodes {
match mix.layer {
LegacyMixLayer::One => layer1_count += 1,
LegacyMixLayer::Two => layer2_count += 1,
LegacyMixLayer::Three => layer3_count += 1,
for nym_node in nym_nodes {
if let Some(described) = described_nodes.get_description(&nym_node.node_id()) {
if described.declared_role.mixnode {
mixnodes_count += 1;
} else if described.declared_role.entry {
gateways_count += 1;
}
}
}
if layer1_count >= minimum_full_routes
&& layer2_count >= minimum_full_routes
&& layer3_count >= minimum_full_routes
{
debug!(
"we have {mixnodes_count} possible mixnodes and {gateways_count} possible gateways"
);
if gateways_count >= minimum_full_routes && mixnodes_count * 3 >= minimum_full_routes {
break;
}
@@ -529,30 +533,41 @@ impl PacketPreparer {
let mixing_nym_nodes = descriptions.mixing_nym_nodes();
let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes();
let (mixnodes, invalid_mixnodes) = self.filter_outdated_and_malformed_mixnodes(mixnodes);
let (gateways, invalid_gateways) = self.filter_outdated_and_malformed_gateways(gateways);
let (mut mixnodes_to_test_details, invalid_mixnodes) =
self.filter_outdated_and_malformed_mixnodes(mixnodes);
let (mut gateways_to_test_details, invalid_gateways) =
self.filter_outdated_and_malformed_gateways(gateways);
let mut tested_mixnodes = mixnodes.iter().map(|node| node.into()).collect::<Vec<_>>();
let mut tested_gateways = gateways.iter().map(|node| node.into()).collect::<Vec<_>>();
// summary of nodes that got tested
let mut mixnodes_under_test = mixnodes_to_test_details
.iter()
.map(|node| node.into())
.collect::<Vec<_>>();
let mut gateways_under_test = gateways_to_test_details
.iter()
.map(|node| node.into())
.collect::<Vec<_>>();
// try to add nym-nodes into the fold
if let Some(rewarded_set) = rewarded_set {
let mut rng = thread_rng();
for mix in mixing_nym_nodes {
if let Some(parsed) = self.nym_node_to_legacy_mix(&mut rng, &rewarded_set, mix) {
tested_mixnodes.push(TestableNode::from(&parsed));
mixnodes_under_test.push(TestableNode::from(&parsed));
mixnodes_to_test_details.push(parsed);
}
}
}
for gateway in gateway_capable_nym_nodes {
if let Some(parsed) = self.nym_node_to_legacy_gateway(gateway) {
tested_gateways.push((&parsed, gateway.node_id).into())
gateways_under_test.push((&parsed, gateway.node_id).into());
gateways_to_test_details.push((parsed, gateway.node_id));
}
}
let packets_to_create = (test_routes.len() * self.per_node_test_packets)
* (tested_mixnodes.len() + tested_gateways.len());
* (mixnodes_under_test.len() + gateways_under_test.len());
info!("Need to create {} mix packets", packets_to_create);
let mut all_gateway_packets = HashMap::new();
@@ -574,7 +589,7 @@ impl PacketPreparer {
#[allow(clippy::unwrap_used)]
let mixnode_test_packets = mix_tester
.mixnodes_test_packets(
&mixnodes,
&mixnodes_to_test_details,
route_ext,
self.per_node_test_packets as u32,
None,
@@ -588,7 +603,7 @@ impl PacketPreparer {
gateway_packets.push_packets(mix_packets);
// and generate test packets for gateways (note the variable recipient)
for (gateway, node_id) in &gateways {
for (gateway, node_id) in &gateways_to_test_details {
let recipient = self.create_packet_sender(gateway);
let gateway_identity = gateway.identity_key;
let gateway_address = gateway.clients_address();
@@ -624,8 +639,8 @@ impl PacketPreparer {
PreparedPackets {
packets,
tested_mixnodes,
tested_gateways,
mixnodes_under_test,
gateways_under_test,
invalid_mixnodes,
invalid_gateways,
}
+2
View File
@@ -145,6 +145,7 @@ impl NodeDescriptionTopologyExt for NymNodeDescription {
}
}
#[derive(Debug, Clone)]
pub struct DescribedNodes {
nodes: HashMap<NodeId, NymNodeDescription>,
}
@@ -290,6 +291,7 @@ async fn try_get_description(
})
}
#[derive(Debug)]
struct RefreshData {
host: String,
node_id: NodeId,
@@ -210,6 +210,7 @@ impl ResolvedNodeDescribedInfo {
}
}
#[derive(Debug)]
pub(crate) struct UnwrappedResolvedNodeDescribedInfo {
pub(crate) build_info: BinaryBuildInformationOwned,
pub(crate) roles: DeclaredRoles,
+3 -3
View File
@@ -209,7 +209,7 @@ pub(crate) async fn produce_node_annotations(
legacy_mix.mix_id(),
NodeAnnotation {
last_24h_performance: perf,
current_role: rewarded_set.role(legacy_mix.mix_id()),
current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()),
},
);
}
@@ -229,7 +229,7 @@ pub(crate) async fn produce_node_annotations(
legacy_gateway.node_id,
NodeAnnotation {
last_24h_performance: perf,
current_role: rewarded_set.role(legacy_gateway.node_id),
current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()),
},
);
}
@@ -249,7 +249,7 @@ pub(crate) async fn produce_node_annotations(
nym_node.node_id(),
NodeAnnotation {
last_24h_performance: perf,
current_role: rewarded_set.role(nym_node.node_id()),
current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()),
},
);
}
+10 -1
View File
@@ -16,6 +16,7 @@ use nym_serde_helpers::date::DATE_FORMAT;
use reqwest::StatusCode;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sqlx::Error;
use std::fmt::Display;
use thiserror::Error;
use time::{Date, OffsetDateTime};
@@ -438,7 +439,7 @@ pub enum NymApiStorageError {
// I don't think we want to expose errors to the user about what really happened
#[error("experienced internal database error")]
InternalDatabaseError(#[from] sqlx::Error),
InternalDatabaseError(sqlx::Error),
// the same is true here (also note that the message is subtly different so we would be able to distinguish them)
#[error("experienced internal storage error")]
@@ -449,6 +450,14 @@ pub enum NymApiStorageError {
StartupMigrationFailure(#[from] sqlx::migrate::MigrateError),
}
impl From<sqlx::Error> for NymApiStorageError {
fn from(err: Error) -> Self {
// those should realistically never be happening so an `error!` is warranted
error!("storage failure: {err}");
NymApiStorageError::InternalDatabaseError(err)
}
}
impl NymApiStorageError {
pub fn database_inconsistency<S: Into<String>>(reason: S) -> NymApiStorageError {
NymApiStorageError::DatabaseInconsistency {
+1 -1
View File
@@ -173,7 +173,7 @@ impl NymContractCacheRefresher {
layer,
},
rewarding_details: detail.rewarding_details,
pending_changes: detail.pending_changes,
pending_changes: detail.pending_changes.into(),
})
}
+1 -1
View File
@@ -49,7 +49,7 @@ async fn get_gateways_described(
.into_iter()
.map(|bond| LegacyDescribedGateway {
self_described: self_descriptions.get_description(&bond.node_id).cloned(),
bond,
bond: bond.bond,
})
.collect(),
)
+7 -12
View File
@@ -9,7 +9,7 @@ use axum::routing::get;
use axum::{Json, Router};
use nym_api_requests::models::{
AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NoiseDetails,
NymNodeData, PerformanceHistoryResponse, UptimeHistoryResponse,
NymNodeDescription, PerformanceHistoryResponse, UptimeHistoryResponse,
};
use nym_api_requests::pagination::{PaginatedResponse, Pagination};
use nym_contracts_common::NaiveFloat;
@@ -125,32 +125,27 @@ async fn get_bonded_nodes(
path = "/described",
context_path = "/v1/nym-nodes",
responses(
(status = 200, body = PaginatedResponse<NymNodeData>)
(status = 200, body = PaginatedResponse<NymNodeDescription>)
),
params(PaginationRequest)
)]
async fn get_described_nodes(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<Json<PaginatedResponse<NymNodeData>>> {
) -> AxumResult<Json<PaginatedResponse<NymNodeDescription>>> {
// TODO: implement it
let _ = pagination;
let cache = state.described_nodes_cache.get().await?;
let descriptions = cache.all_nodes();
let data = descriptions
.map(|n| &n.description)
.cloned()
.collect::<Vec<_>>();
let descriptions = cache.all_nodes().cloned().collect::<Vec<_>>();
Ok(Json(PaginatedResponse {
pagination: Pagination {
total: data.len(),
total: descriptions.len(),
page: 0,
size: data.len(),
size: descriptions.len(),
},
data,
data: descriptions,
}))
}
+2 -2
View File
@@ -234,9 +234,9 @@ impl Config {
fn default_http_socket_addr() -> SocketAddr {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080)
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8000)
} else {
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080)
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8000)
}
}
}
+4 -5
View File
@@ -119,9 +119,8 @@ impl StorageManager {
start_ts_secs: i64,
end_ts_secs: i64,
) -> Result<Vec<AvgGatewayReliability>, sqlx::Error> {
// we can't use `query_as!` macro because we don't apply all required table changes during sqlx migrations.
// some (like v3 directory) happens at runtime
let result = sqlx::query_as(
let result = sqlx::query_as!(
AvgGatewayReliability,
r#"
SELECT
d.node_id as "node_id: NodeId",
@@ -135,9 +134,9 @@ impl StorageManager {
timestamp <= ?
GROUP BY 1
"#,
start_ts_secs,
end_ts_secs
)
.bind(start_ts_secs)
.bind(end_ts_secs)
.fetch_all(&self.connection_pool)
.await?;
Ok(result)
-4
View File
@@ -30,10 +30,6 @@ pub async fn migrate_v3_database(
let contract_gateways = nyxd_client.get_gateways().await?;
let nym_nodes = nyxd_client.get_nymnodes().await?;
if preassigned_ids.len() != contract_gateways.len() {
bail!("CONTRACT DATA CORRUPTION: THE NUMBER OF PREASSIGNED GATEWAY IDS IS DIFFERENT THAN THE NUMBER OF GATEWAYS")
}
// assign node_id to every gateway
let all_known = storage.get_all_known_gateways().await?;
for gateway in all_known {
@@ -22,12 +22,12 @@ reqwest = { workspace = true, features = ["json"] }
wasm-bindgen = { workspace = true, optional = true }
## openapi:
utoipa = { workspace = true, optional = true }
utoipa = { workspace = true, optional = true, features = ["uuid"] }
nym-credentials = { path = "../../common/credentials" }
nym-credentials-interface = { path = "../../common/credentials-interface" }
nym-http-api-common = { path = "../../common/http-api-common", optional = true }
nym-http-api-client = { path = "../../common/http-api-client" }
nym-http-api-client = { path = "../../common/http-api-client" }
nym-serde-helpers = { path = "../../common/serde-helpers", features = ["bs58"] }
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
@@ -40,6 +40,7 @@ pub struct TicketbookRequest {
/// you **MUST** provide a valid value otherwise blacklisting won't work
#[schemars(with = "String")]
#[serde(with = "bs58_ecash")]
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub ecash_pubkey: PublicKeyUser,
// needs to be explicit in case user creates request at 23:59:59.999, but it reaches vpn-api at 00:00:00.001
@@ -48,6 +49,7 @@ pub struct TicketbookRequest {
pub expiration_date: Date,
#[schemars(with = "String")]
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub ticketbook_type: TicketType,
pub is_freepass_request: bool,
@@ -1,6 +1,6 @@
[package]
name = "nym-credential-proxy"
version = "0.1.1"
version = "0.1.2"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -0,0 +1,20 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
DROP TABLE blinded_shares;
CREATE TABLE blinded_shares
(
id INTEGER NOT NULL PRIMARY KEY,
-- removed reference to `ticketbook_deposit` as the deposit wouldn't actually have been made before the pending share is inserted
request_uuid TEXT NOT NULL,
status TEXT NOT NULL,
device_id TEXT NOT NULL,
credential_id TEXT NOT NULL,
available_shares INTEGER NOT NULL DEFAULT 0,
error_message TEXT DEFAULT NULL,
created TIMESTAMP WITHOUT TIME ZONE NOT NULL,
updated TIMESTAMP WITHOUT TIME ZONE NOT NULL
);
@@ -149,7 +149,7 @@ pub(crate) async fn query_for_shares_by_id(
(status = 401, description = "authentication token is missing or is invalid"),
(status = 500, body = ErrorResponse, description = "failed to query for bandwidth blinded shares"),
),
params(OutputParams),
params(SharesQueryParams),
security(
("auth_token" = [])
)
@@ -82,7 +82,11 @@ impl SqliteStorageManager {
sqlx::query_as!(
MinimalWalletShare,
r#"
SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date
SELECT
t1.node_id as "node_id!",
t1.blinded_signature as "blinded_signature!",
t1.epoch_id as "epoch_id!",
t1.expiration_date as "expiration_date!"
FROM partial_blinded_wallet as t1
JOIN ticketbook_deposit as t2
on t1.corresponding_deposit = t2.deposit_id
+89 -3
View File
@@ -1,7 +1,9 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::api::v1::node::models::{HostInformation, LegacyHostInformation};
use crate::api::v1::node::models::{
HostInformation, LegacyHostInformation, LegacyHostInformationV2,
};
use crate::error::Error;
use nym_crypto::asymmetric::identity;
use schemars::JsonSchema;
@@ -61,9 +63,18 @@ impl SignedHostInformation {
return true;
}
// attempt to verify legacy signature
// attempt to verify legacy signatures
let legacy_v2 = SignedData {
data: LegacyHostInformationV2::from(self.data.clone()),
signature: self.signature.clone(),
};
if legacy_v2.verify(&self.keys.ed25519_identity) {
return true;
}
SignedData {
data: LegacyHostInformation::from(self.data.clone()),
data: LegacyHostInformation::from(legacy_v2.data),
signature: self.signature.clone(),
}
.verify(&self.keys.ed25519_identity)
@@ -133,6 +144,81 @@ mod tests {
assert!(signed_info.verify_host_information());
}
#[test]
fn dummy_legacy_v2_signed_host_verification() {
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
let ed22519 = ed25519::KeyPair::new(&mut rng);
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
let x25519_noise = x25519::KeyPair::new(&mut rng);
let legacy_info_no_noise = crate::api::v1::node::models::LegacyHostInformationV2 {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::LegacyHostKeysV2 {
ed25519_identity: ed22519.public_key().to_base58_string(),
x25519_sphinx: x25519_sphinx.public_key().to_base58_string(),
x25519_noise: "".to_string(),
},
};
let legacy_info_noise = crate::api::v1::node::models::LegacyHostInformationV2 {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::LegacyHostKeysV2 {
ed25519_identity: ed22519.public_key().to_base58_string(),
x25519_sphinx: x25519_sphinx.public_key().to_base58_string(),
x25519_noise: x25519_noise.public_key().to_base58_string(),
},
};
let host_info_no_noise = crate::api::v1::node::models::HostInformation {
ip_address: legacy_info_no_noise.ip_address.clone(),
hostname: legacy_info_no_noise.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info_no_noise.keys.ed25519_identity.parse().unwrap(),
x25519_sphinx: legacy_info_no_noise.keys.x25519_sphinx.parse().unwrap(),
x25519_noise: None,
},
};
let host_info_noise = crate::api::v1::node::models::HostInformation {
ip_address: legacy_info_noise.ip_address.clone(),
hostname: legacy_info_noise.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info_noise.keys.ed25519_identity.parse().unwrap(),
x25519_sphinx: legacy_info_noise.keys.x25519_sphinx.parse().unwrap(),
x25519_noise: Some(legacy_info_noise.keys.x25519_noise.parse().unwrap()),
},
};
// signature on legacy data
let signature_no_noise = SignedData::new(legacy_info_no_noise, ed22519.private_key())
.unwrap()
.signature;
let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key())
.unwrap()
.signature;
// signed blob with the 'current' structure
let current_struct_no_noise = SignedData {
data: host_info_no_noise,
signature: signature_no_noise,
};
let current_struct_noise = SignedData {
data: host_info_noise,
signature: signature_noise,
};
assert!(!current_struct_no_noise.verify(ed22519.public_key()));
assert!(current_struct_no_noise.verify_host_information());
// if noise key is present, the signature is actually valid
assert!(current_struct_noise.verify(ed22519.public_key()));
assert!(current_struct_noise.verify_host_information())
}
#[test]
fn dummy_legacy_signed_host_verification() {
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
@@ -59,6 +59,13 @@ pub struct HostInformation {
pub keys: HostKeys,
}
#[derive(Serialize)]
pub struct LegacyHostInformationV2 {
pub ip_address: Vec<IpAddr>,
pub hostname: Option<String>,
pub keys: LegacyHostKeysV2,
}
#[derive(Serialize)]
pub struct LegacyHostInformation {
pub ip_address: Vec<IpAddr>,
@@ -66,8 +73,18 @@ pub struct LegacyHostInformation {
pub keys: LegacyHostKeys,
}
impl From<HostInformation> for LegacyHostInformation {
impl From<HostInformation> for LegacyHostInformationV2 {
fn from(value: HostInformation) -> Self {
LegacyHostInformationV2 {
ip_address: value.ip_address,
hostname: value.hostname,
keys: value.keys.into(),
}
}
}
impl From<LegacyHostInformationV2> for LegacyHostInformation {
fn from(value: LegacyHostInformationV2) -> Self {
LegacyHostInformation {
ip_address: value.ip_address,
hostname: value.hostname,
@@ -99,13 +116,11 @@ pub struct HostKeys {
pub x25519_noise: Option<x25519::PublicKey>,
}
impl From<HostKeys> for LegacyHostKeys {
fn from(value: HostKeys) -> Self {
LegacyHostKeys {
ed25519: value.ed25519_identity.to_base58_string(),
x25519: value.x25519_sphinx.to_base58_string(),
}
}
#[derive(Serialize)]
pub struct LegacyHostKeysV2 {
pub ed25519_identity: String,
pub x25519_sphinx: String,
pub x25519_noise: String,
}
#[derive(Serialize)]
@@ -114,6 +129,28 @@ pub struct LegacyHostKeys {
pub x25519: String,
}
impl From<HostKeys> for LegacyHostKeysV2 {
fn from(value: HostKeys) -> Self {
LegacyHostKeysV2 {
ed25519_identity: value.ed25519_identity.to_base58_string(),
x25519_sphinx: value.x25519_sphinx.to_base58_string(),
x25519_noise: value
.x25519_noise
.map(|k| k.to_base58_string())
.unwrap_or_default(),
}
}
}
impl From<LegacyHostKeysV2> for LegacyHostKeys {
fn from(value: LegacyHostKeysV2) -> Self {
LegacyHostKeys {
ed25519: value.ed25519_identity,
x25519: value.x25519_sphinx,
}
}
}
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct HostSystem {
+5 -1
View File
@@ -154,7 +154,7 @@ pub fn ephemeral_entry_gateway_config(
config: Config,
mnemonic: &bip39::Mnemonic,
) -> Result<EphemeralConfig, EntryGatewayError> {
let auth_opts = LocalAuthenticatorOpts {
let mut auth_opts = LocalAuthenticatorOpts {
config: nym_authenticator::Config {
base: nym_client_core_config_types::Config {
client: base_client_config(&config),
@@ -173,6 +173,10 @@ pub fn ephemeral_entry_gateway_config(
custom_mixnet_path: None,
};
if config.authenticator.debug.disable_poisson_rate {
auth_opts.config.base.set_no_poisson_process();
}
let wg_opts = LocalWireguardOpts {
config: super::Wireguard {
enabled: config.wireguard.enabled,
+5 -1
View File
@@ -243,7 +243,7 @@ pub fn ephemeral_exit_gateway_config(
ipr_opts.config.base.set_no_poisson_process()
}
let auth_opts = LocalAuthenticatorOpts {
let mut auth_opts = LocalAuthenticatorOpts {
config: nym_authenticator::Config {
base: nym_client_core_config_types::Config {
client: base_client_config(&config),
@@ -262,6 +262,10 @@ pub fn ephemeral_exit_gateway_config(
custom_mixnet_path: None,
};
if config.authenticator.debug.disable_poisson_rate {
auth_opts.config.base.set_no_poisson_process();
}
let pub_id_path = config
.storage_paths
.keys
+1
View File
@@ -46,6 +46,7 @@ pub struct Verloc {
/// will use.
/// Useful when the node is behind a proxy.
#[serde(deserialize_with = "de_maybe_port")]
#[serde(default)]
pub announce_port: Option<u16>,
#[serde(default)]
+1
View File
@@ -421,6 +421,7 @@ pub struct Mixnet {
/// will use.
/// Useful when the node is behind a proxy.
#[serde(deserialize_with = "de_maybe_port")]
#[serde(default)]
pub announce_port: Option<u16>,
/// Addresses to nym APIs from which the node gets the view of the network.
+34 -34
View File
@@ -173,7 +173,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -658,7 +658,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -1022,7 +1022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -1083,7 +1083,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -1751,7 +1751,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -1931,7 +1931,7 @@ dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -3667,7 +3667,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -3854,7 +3854,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -3983,7 +3983,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -4140,7 +4140,7 @@ dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -4151,9 +4151,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.79"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
@@ -4178,7 +4178,7 @@ dependencies = [
"itertools 0.11.0",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -4704,7 +4704,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -4798,9 +4798,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
dependencies = [
"serde_derive",
]
@@ -4825,13 +4825,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.214"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -4842,14 +4842,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
name = "serde_json"
version = "1.0.128"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa 1.0.9",
"memchr",
@@ -4865,7 +4865,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -5219,7 +5219,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -5262,9 +5262,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.55"
version = "2.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0"
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
dependencies = [
"proc-macro2",
"quote",
@@ -5769,7 +5769,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -5856,7 +5856,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -6021,7 +6021,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -6097,7 +6097,7 @@ checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
"termcolor",
]
@@ -6201,7 +6201,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -6316,7 +6316,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
"wasm-bindgen-shared",
]
@@ -6350,7 +6350,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -6979,7 +6979,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
"syn 2.0.85",
]
[[package]]
@@ -4,13 +4,12 @@
use crate::api_client;
use crate::error::BackendError;
use crate::state::WalletState;
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::{
reward_params::Performance, Coin, IdentityKeyRef, NodeId, Percent,
};
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::models::{
AnnotationResponse, ComputeRewardEstParam, GatewayCoreStatusResponse,
AnnotationResponse, ComputeRewardEstParam, DisplayRole, GatewayCoreStatusResponse,
GatewayStatusReportResponse, InclusionProbabilityResponse, MixnodeCoreStatusResponse,
MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse,
};
@@ -110,7 +109,7 @@ pub async fn mixnode_inclusion_probability(
pub async fn get_nymnode_role(
node_id: NodeId,
state: tauri::State<'_, WalletState>,
) -> Result<Option<Role>, BackendError> {
) -> Result<Option<DisplayRole>, BackendError> {
let annotation = get_nymnode_annotation(node_id, state).await?;
Ok(annotation.annotation.and_then(|n| n.current_role))
}
@@ -116,12 +116,12 @@ pub async fn generate_gateway_bonding_msg_payload(
#[tauri::command]
pub async fn generate_nym_node_bonding_msg_payload(
nym_node: NymNode,
nymnode: NymNode,
cost_params: NodeCostParams,
pledge: DecCoin,
state: tauri::State<'_, WalletState>,
) -> Result<String, BackendError> {
nym_node_bonding_msg_payload(nym_node, cost_params, pledge, state).await
nym_node_bonding_msg_payload(nymnode, cost_params, pledge, state).await
}
#[tauri::command]
@@ -2,15 +2,15 @@ import React, { createContext, useContext, useMemo, useState } from 'react';
import { CurrencyDenom } from '@nymproject/types';
import { TBondNymNodeArgs, TBondMixNodeArgs } from 'src/types';
const defaultNymNodeValues: TBondNymNodeArgs['nymNode'] = {
identity_key: 'H6rXWgsW89QsVyaNSS3qBe9zZFLhBS6Gn3YRkGFSoFW9',
custom_http_port: 1,
const defaultNymNodeValues: TBondNymNodeArgs['nymnode'] = {
identity_key: '',
custom_http_port: null,
host: '1.1.1.1',
};
const defaultCostParams = (denom: CurrencyDenom): TBondNymNodeArgs['costParams'] => ({
interval_operating_cost: { amount: '40', denom },
profit_margin_percent: '10',
profit_margin_percent: '40',
});
const defaultAmount = (denom: CurrencyDenom): TBondMixNodeArgs['pledge'] => ({
@@ -21,14 +21,14 @@ const defaultAmount = (denom: CurrencyDenom): TBondMixNodeArgs['pledge'] => ({
interface FormContextType {
step: 1 | 2 | 3 | 4;
setStep: React.Dispatch<React.SetStateAction<1 | 2 | 3 | 4>>;
nymNodeData: TBondNymNodeArgs['nymNode'];
setNymNodeData: React.Dispatch<React.SetStateAction<TBondNymNodeArgs['nymNode']>>;
nymNodeData: TBondNymNodeArgs['nymnode'];
setNymNodeData: React.Dispatch<React.SetStateAction<TBondNymNodeArgs['nymnode']>>;
costParams: TBondNymNodeArgs['costParams'];
setCostParams: React.Dispatch<React.SetStateAction<TBondNymNodeArgs['costParams']>>;
amountData: TBondMixNodeArgs['pledge'];
setAmountData: React.Dispatch<React.SetStateAction<TBondMixNodeArgs['pledge']>>;
signature?: string;
setSignature: React.Dispatch<React.SetStateAction<string | undefined>>;
signature: string;
setSignature: React.Dispatch<React.SetStateAction<string>>;
onError: (e: string) => void;
}
@@ -41,9 +41,8 @@ const FormContext = createContext<FormContextType>({
setCostParams: () => {},
amountData: defaultAmount('nym'),
setAmountData: () => {},
signature: undefined,
signature: '',
setSignature: () => {},
onError: () => {},
});
@@ -52,10 +51,10 @@ const FormContextProvider = ({ children }: { children: React.ReactNode }) => {
const denom = 'nym';
const [step, setStep] = useState<1 | 2 | 3 | 4>(1);
const [nymNodeData, setNymNodeData] = useState<TBondNymNodeArgs['nymNode']>(defaultNymNodeValues);
const [nymNodeData, setNymNodeData] = useState<TBondNymNodeArgs['nymnode']>(defaultNymNodeValues);
const [costParams, setCostParams] = useState<TBondNymNodeArgs['costParams']>(defaultCostParams(denom));
const [amountData, setAmountData] = useState<TBondNymNodeArgs['pledge']>(defaultAmount(denom));
const [signature, setSignature] = useState<string>();
const [signature, setSignature] = useState('');
const onError = (e: string) => {
console.error(e);
@@ -1,22 +1,12 @@
import React from 'react';
import { Stack, TextField, Box, FormHelperText } from '@mui/material';
import { useForm } from 'react-hook-form';
import { TBondNymNodeArgs } from 'src/types';
import { yupResolver } from '@hookform/resolvers/yup';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { checkHasEnoughFunds } from 'src/utils';
import { nymNodeAmountSchema } from './amountValidationSchema';
const defaultNymNodeCostParamValues: TBondNymNodeArgs['costParams'] = {
profit_margin_percent: '10',
interval_operating_cost: { amount: '40', denom: 'nym' },
};
const defaultNymNodePledgeValue: TBondNymNodeArgs['pledge'] = {
amount: '100',
denom: 'nym',
};
import { useFormContext } from './FormContext';
type NymNodeDataProps = {
onClose: () => void;
@@ -26,6 +16,7 @@ type NymNodeDataProps = {
};
const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => {
const { setAmountData, setCostParams, amountData, costParams } = useFormContext();
const {
formState: { errors },
register,
@@ -36,21 +27,26 @@ const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => {
} = useForm({
mode: 'all',
defaultValues: {
pledge: defaultNymNodePledgeValue,
...defaultNymNodeCostParamValues,
pledge: amountData,
...costParams,
},
resolver: yupResolver(nymNodeAmountSchema()),
});
console.log(errors, 'errors');
const handleRequestValidation = async () => {
const values = getValues();
const hasSufficientTokens = await checkHasEnoughFunds(values.pledge.amount);
if (hasSufficientTokens) {
handleSubmit(onNext)();
handleSubmit((args) => {
setAmountData(args.pledge);
setCostParams({
profit_margin_percent: args.profit_margin_percent,
interval_operating_cost: args.interval_operating_cost,
});
onNext();
})();
} else {
setError('pledge.amount', { message: 'Not enough tokens' });
}
@@ -77,8 +73,8 @@ const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => {
setValue('pledge.amount', newValue.amount, { shouldValidate: true });
}}
validationError={errors.pledge?.amount?.message}
denom={defaultNymNodePledgeValue.denom}
initialValue={defaultNymNodePledgeValue.amount}
denom={amountData.denom}
initialValue={amountData.amount}
/>
<Box>
@@ -90,8 +86,8 @@ const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => {
setValue('interval_operating_cost', newValue, { shouldValidate: true });
}}
validationError={errors.interval_operating_cost?.amount?.message}
denom={defaultNymNodeCostParamValues.interval_operating_cost.denom}
initialValue={defaultNymNodeCostParamValues.interval_operating_cost.amount}
denom={costParams.interval_operating_cost.denom}
initialValue={costParams.interval_operating_cost.amount}
/>
<FormHelperText>
Monthly operational costs of running your node. If your node is in the active set the amount will be paid
@@ -1,32 +1,12 @@
import React from 'react';
import * as Yup from 'yup';
import { Stack, TextField, FormControlLabel, Checkbox } from '@mui/material';
import { useForm } from 'react-hook-form';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { TBondNymNodeArgs } from 'src/types';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { isValidHostname, validateRawPort } from 'src/utils';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
const defaultNymNodeValues: TBondNymNodeArgs['nymNode'] = {
identity_key: 'H6rXWgsW89QsVyaNSS3qBe9zZFLhBS6Gn3YRkGFSoFW9',
custom_http_port: 1,
host: '1.1.1.1',
};
const yupValidationSchema = yup.object().shape({
identity_key: yup.string().required('Identity key is required'),
host: yup
.string()
.required('A host is required')
.test('no-whitespace', 'Host cannot contain whitespace', (value) => !/\s/.test(value || ''))
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
custom_http_port: yup
.number()
.required('A custom http port is required')
.test('valid-http', 'A valid http port is required', (value) => (value ? validateRawPort(value) : false)),
});
import { useFormContext } from './FormContext';
import { settingsValidationSchema } from './settingsValidationSchema';
type NymNodeDataProps = {
onClose: () => void;
@@ -35,7 +15,13 @@ type NymNodeDataProps = {
step: number;
};
const validationSchema = Yup.object().shape({
identity_key: Yup.string().required('Identity key is required'),
...settingsValidationSchema.fields,
});
const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => {
const { setNymNodeData, nymNodeData } = useFormContext();
const {
formState: { errors },
register,
@@ -43,14 +29,17 @@ const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => {
handleSubmit,
} = useForm({
mode: 'all',
defaultValues: defaultNymNodeValues,
resolver: yupResolver(yupValidationSchema),
defaultValues: nymNodeData,
resolver: yupResolver(validationSchema),
});
const [showAdvancedOptions, setShowAdvancedOptions] = React.useState(false);
const handleNext = async () => {
handleSubmit(onNext)();
handleSubmit((args) => {
setNymNodeData(args);
onNext();
})();
};
return (
@@ -69,7 +58,7 @@ const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => {
required
fullWidth
label="Identity Key"
initialValue={defaultNymNodeValues.identity_key}
initialValue={nymNodeData.identity_key}
errorText={errors.identity_key?.message?.toString()}
onChanged={(value) => setValue('identity_key', value, { shouldValidate: true })}
showTickOnValid={false}
@@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import * as yup from 'yup';
import { Stack, TextField, Typography } from '@mui/material';
import { useForm } from 'react-hook-form';
import { CopyToClipboard } from 'src/components/CopyToClipboard';
@@ -7,9 +8,11 @@ import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { useBondingContext } from 'src/context';
import { TBondNymNodeArgs } from 'src/types';
import { Signature } from 'src/pages/bonding/types';
import { yupResolver } from '@hookform/resolvers/yup';
import { useFormContext } from './FormContext';
const NymNodeSignature = ({
nymNode,
nymnode,
pledge,
costParams,
step,
@@ -17,27 +20,38 @@ const NymNodeSignature = ({
onClose,
onBack,
}: {
nymNode: TBondNymNodeArgs['nymNode'];
nymnode: TBondNymNodeArgs['nymnode'];
pledge: TBondNymNodeArgs['pledge'];
costParams: TBondNymNodeArgs['costParams'];
step: number;
onNext: (data: Signature) => void;
onNext: () => void;
onClose: () => void;
onBack: () => void;
}) => {
const [message, setMessage] = useState<string>('');
const [error, setError] = useState<string>();
const { generateNymNodeMsgPayload } = useBondingContext();
const { signature, setSignature } = useFormContext();
const yupValidationSchema = yup.object().shape({
signature: yup.string().required('Signature is required'),
});
const {
register,
formState: { errors },
} = useForm<Signature>();
handleSubmit,
} = useForm<Signature>({
defaultValues: {
signature,
},
resolver: yupResolver(yupValidationSchema),
});
const generateMessage = async () => {
try {
const msg = await generateNymNodeMsgPayload({
nymNode,
nymnode,
pledge,
costParams,
});
@@ -53,10 +67,10 @@ const NymNodeSignature = ({
useEffect(() => {
generateMessage();
}, [nymNode, pledge, costParams]);
}, []);
const onSubmit = async (data: Signature) => {
onNext(data);
const handleNext = async () => {
handleSubmit(onNext)();
};
if (error) {
@@ -66,11 +80,7 @@ const NymNodeSignature = ({
return (
<SimpleModal
open
onOk={() =>
onSubmit({
signature: 'signature',
})
}
onOk={handleNext}
onClose={onClose}
header="Bond Nym Node"
subHeader={`Step ${step}/3`}
@@ -95,10 +105,13 @@ const NymNodeSignature = ({
</Stack>
<TextField
{...register('signature')}
onChange={(e) => setSignature(e.target.value)}
id="outlined-multiline-static"
name="signature"
rows={3}
placeholder="Paste Signature"
helperText={errors.signature?.message}
error={Boolean(errors.signature)}
multiline
fullWidth
required
@@ -5,8 +5,8 @@ import { isLessThan, isGreaterThan, validateAmount } from 'src/utils';
const operatingCostAndPmValidation = (params?: TauriContractStateParams) => {
const defaultParams = {
profit_margin_percent: {
minimum: parseFloat(params?.profit_margin.minimum || '0%'),
maximum: parseFloat(params?.profit_margin.maximum || '100%'),
minimum: parseFloat(params?.profit_margin.minimum || '20%'),
maximum: parseFloat(params?.profit_margin.maximum || '50%'),
},
interval_operating_cost: {
@@ -0,0 +1,20 @@
import { isValidHostname, validateRawPort } from 'src/utils';
import * as Yup from 'yup';
const settingsValidationSchema = Yup.object().shape({
host: Yup.string()
.required('A host is required')
.test('no-whitespace', 'Host cannot contain whitespace', (value) => !/\s/.test(value || ''))
.test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)),
custom_http_port: Yup.number()
.nullable()
.test('valid-http', 'A valid http port is required', (value) => {
if (value === null) {
return true;
}
return value ? validateRawPort(value) : false;
}),
});
export { settingsValidationSchema };
@@ -2,18 +2,24 @@ import React, { useContext, useEffect } from 'react';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { ModalListItem } from 'src/components/Modals/ModalListItem';
import { useGetFee } from 'src/hooks/useGetFee';
import { Signature } from 'src/pages/bonding/types';
import { BalanceWarning } from 'src/components/FeeWarning';
import { AppContext } from 'src/context';
import { TBondNymNodeArgs } from 'src/types';
import FormContextProvider, { useFormContext } from '../forms/nym-node/FormContext';
import NymNodeData from '../forms/nym-node/NymNodeData';
import NymNodeAmount from '../forms/nym-node/NymNodeAmount';
import NymNodeSignature from '../forms/nym-node/NymNodeSignature';
export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => {
export const BondNymNodeModal = ({
onClose,
onBond,
}: {
onClose: () => void;
onBond: (data: TBondNymNodeArgs) => Promise<void>;
}) => {
const { fee, resetFeeState, feeError } = useGetFee();
const { userBalance } = useContext(AppContext);
const { setStep, step, onError, setSignature, amountData, costParams, nymNodeData } = useFormContext();
const { setStep, step, onError, signature, amountData, costParams, nymNodeData } = useFormContext();
useEffect(() => {
if (feeError) {
@@ -21,12 +27,17 @@ export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => {
}
}, [feeError]);
const handleUpdateMixnodeData = async () => {
const handleUpdateNymnodeData = async () => {
setStep(2);
};
const handleUpdateSignature = async (data: Signature) => {
setSignature(data.signature);
const handleBond = async () => {
onBond({
nymnode: nymNodeData,
pledge: amountData,
costParams,
msgSignature: signature,
});
};
const handleConfirm = async () => {};
@@ -51,7 +62,7 @@ export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => {
}
if (step === 1) {
return <NymNodeData onClose={onClose} onBack={onClose} onNext={handleUpdateMixnodeData} step={step} />;
return <NymNodeData onClose={onClose} onBack={onClose} onNext={handleUpdateNymnodeData} step={step} />;
}
if (step === 2) {
@@ -61,10 +72,10 @@ export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => {
if (step === 3) {
return (
<NymNodeSignature
nymNode={nymNodeData}
nymnode={nymNodeData}
pledge={amountData}
costParams={costParams}
onNext={handleUpdateSignature}
onNext={handleBond}
onClose={onClose}
onBack={() => setStep(2)}
step={step}
@@ -75,14 +86,22 @@ export const BondNymNodeModal = ({ onClose }: { onClose: () => void }) => {
return null;
};
export const BondNymNodeModalWithState = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
export const BondNymNode = ({
open,
onClose,
onBond,
}: {
open: boolean;
onClose: () => void;
onBond: (data: TBondNymNodeArgs) => Promise<void>;
}) => {
if (!open) {
return null;
}
return (
<FormContextProvider>
<BondNymNodeModal onClose={onClose} />
<BondNymNodeModal onClose={onClose} onBond={onBond} />
</FormContextProvider>
);
};
@@ -1,10 +1,9 @@
import * as React from 'react';
import { useEffect } from 'react';
import { Typography } from '@mui/material';
import { TBondedNode } from 'src/context';
import { useGetFee } from 'src/hooks/useGetFee';
import { isGateway, isMixnode } from 'src/types';
import { TBondedGateway } from 'src/requests/gatewayDetails';
import { TBondedMixnode } from 'src/requests/mixnodeDetails';
import { ModalFee } from '../../Modals/ModalFee';
import { ModalListItem } from '../../Modals/ModalListItem';
import { SimpleModal } from '../../Modals/SimpleModal';
@@ -16,7 +15,7 @@ import {
} from '../../../requests';
interface Props {
node: TBondedMixnode | TBondedGateway;
node: TBondedNode;
onConfirm: () => Promise<void>;
onClose: () => void;
onError: (e: string) => void;
+63 -3
View File
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { FeeDetails, TransactionExecuteResult } from '@nymproject/types';
import { isGateway, isMixnode, TUpdateBondArgs, isNymNode, TNymNodeSignatureArgs } from 'src/types';
import { isGateway, isMixnode, TUpdateBondArgs, isNymNode, TNymNodeSignatureArgs, TBondNymNodeArgs } from 'src/types';
import { Console } from 'src/utils/console';
import useGetNodeDetails from 'src/hooks/useGetNodeDetails';
import { TBondedNymNode } from 'src/requests/nymNodeDetails';
@@ -20,6 +20,8 @@ import {
migrateVestedMixnode as tauriMigrateVestedMixnode,
migrateLegacyMixnode as migrateLegacyMixnodeReq,
migrateLegacyGateway as migrateLegacyGatewayReq,
bondNymNode,
updateNymNodeConfig as updateNymNodeConfigReq,
} from '../requests';
export type TBondedNode = TBondedMixnode | TBondedGateway | TBondedNymNode;
@@ -31,7 +33,12 @@ export type TBondingContext = {
isVestingAccount: boolean;
refresh: () => void;
unbond: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
bond: (args: TBondNymNodeArgs) => Promise<TransactionExecuteResult | undefined>;
updateBondAmount: (data: TUpdateBondArgs) => Promise<TransactionExecuteResult | undefined>;
updateNymNodeConfig: (data: {
host: string;
custom_http_port: number | null;
}) => Promise<TransactionExecuteResult | undefined>;
redeemRewards: (fee?: FeeDetails) => Promise<TransactionExecuteResult | undefined>;
generateNymNodeMsgPayload: (data: TNymNodeSignatureArgs) => Promise<string | undefined>;
migrateVestedMixnode: () => Promise<TransactionExecuteResult | undefined>;
@@ -41,12 +48,18 @@ export type TBondingContext = {
export const BondingContext = createContext<TBondingContext>({
isLoading: true,
refresh: async () => undefined,
bond: async () => {
throw new Error('Not implemented');
},
unbond: async () => {
throw new Error('Not implemented');
},
updateBondAmount: async () => {
throw new Error('Not implemented');
},
updateNymNodeConfig: async () => {
throw new Error('Not implemented');
},
redeemRewards: async () => {
throw new Error('Not implemented');
},
@@ -70,7 +83,11 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen
const { userBalance, clientDetails, network } = useContext(AppContext);
const { bondedNode, isLoading: isBondedNodeLoading } = useGetNodeDetails(clientDetails?.client_address, network);
const {
bondedNode,
isLoading: isBondedNodeLoading,
getNodeDetails,
} = useGetNodeDetails(clientDetails?.client_address, network);
useEffect(() => {
userBalance.fetchBalance();
@@ -91,6 +108,30 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen
resetState();
};
const bond = async (data: TBondNymNodeArgs) => {
let tx;
setIsLoading(true);
try {
tx = await bondNymNode({
...data,
costParams: {
...data.costParams,
profit_margin_percent: toPercentFloatString(data.costParams.profit_margin_percent),
},
});
if (clientDetails?.client_address) {
await getNodeDetails(clientDetails?.client_address);
}
} catch (e) {
Console.warn(e);
setError(`an error occurred: ${e as string}`);
} finally {
setIsLoading(false);
}
return tx;
};
const unbond = async (fee?: FeeDetails) => {
let tx;
setIsLoading(true);
@@ -107,6 +148,23 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen
return tx;
};
const updateNymNodeConfig = async (data: { host: string; custom_http_port: number | null }) => {
let tx;
setIsLoading(true);
try {
tx = await updateNymNodeConfigReq(data);
if (clientDetails?.client_address) {
await getNodeDetails(clientDetails?.client_address);
}
} catch (e) {
Console.warn(e);
setError(`an error occurred: ${e}`);
} finally {
setIsLoading(false);
}
return tx;
};
const redeemRewards = async (fee?: FeeDetails) => {
let tx;
setIsLoading(true);
@@ -143,7 +201,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen
try {
const message = await generateNymNodeMsgPayloadReq({
nymNode: data.nymNode,
nymnode: data.nymnode,
pledge: data.pledge,
costParams: {
...data.costParams,
@@ -187,10 +245,12 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen
isLoading: isLoading || isBondedNodeLoading,
error,
bondedNode,
bond,
unbond,
refresh,
redeemRewards,
updateBondAmount,
updateNymNodeConfig,
generateNymNodeMsgPayload,
migrateVestedMixnode,
migrateLegacyNode,
+18
View File
@@ -133,6 +133,14 @@ export const MockBondingContextProvider = ({
return TxResultMock;
};
const bond = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
setBondedData(bondedMixnodeMock);
setIsLoading(false);
return TxResultMock;
};
const unbond = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
@@ -141,6 +149,14 @@ export const MockBondingContextProvider = ({
return TxResultMock;
};
const updateNymNodeConfig = async (): Promise<TransactionExecuteResult> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
triggerStateUpdate();
setIsLoading(false);
return TxResultMock;
};
const redeemRewards = async (): Promise<TransactionExecuteResult | undefined> => {
setIsLoading(true);
await mockSleep(SLEEP_MS);
@@ -189,6 +205,7 @@ export const MockBondingContextProvider = ({
error,
bondMixnode,
bondGateway,
bond,
unbond,
refresh,
redeemRewards,
@@ -203,6 +220,7 @@ export const MockBondingContextProvider = ({
isVestingAccount: false,
migrateVestedMixnode: async () => undefined,
migrateLegacyNode: async () => undefined,
updateNymNodeConfig,
}),
[isLoading, error, bondedMixnode, bondedGateway, trigger, fee],
);
@@ -63,6 +63,7 @@ const useGetNodeDetails = (clientAddress?: string, network?: string) => {
bondedNode,
isLoading,
isError,
getNodeDetails,
};
};
+16 -3
View File
@@ -11,14 +11,14 @@ import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/component
import { ErrorModal } from 'src/components/Modals/ErrorModal';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { AppContext, urls } from 'src/context/main';
import { isGateway, isMixnode, isNymNode, TUpdateBondArgs } from 'src/types';
import { isGateway, isMixnode, isNymNode, TBondNymNodeArgs, TUpdateBondArgs } from 'src/types';
import { BondedGateway } from 'src/components/Bonding/BondedGateway';
import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal';
import { VestingWarningModal } from 'src/components/VestingWarningModal';
import MigrateLegacyNode from 'src/components/Bonding/modals/MigrateLegacyNode';
import { BondedNymNode } from 'src/components/Bonding/BondedNymNode';
import { UpdateBondAmountNymNode } from 'src/components/Bonding/modals/UpdateBondAmountNymNode';
import { BondNymNodeModalWithState } from 'src/components/Bonding/modals/BondNymNodeModal';
import { BondNymNode } from 'src/components/Bonding/modals/BondNymNodeModal';
import { BondingContextProvider, useBondingContext } from '../../context';
export const Bonding = () => {
@@ -44,6 +44,7 @@ export const Bonding = () => {
redeemRewards,
updateBondAmount,
refresh,
bond,
migrateVestedMixnode,
migrateLegacyNode,
} = useBondingContext();
@@ -74,6 +75,18 @@ export const Bonding = () => {
setShowMigrateLegacyNodeModal(shouldShowMigrateLegacyNodeModal());
}, [bondedNode]);
const handleBondNymNode = async (data: TBondNymNodeArgs) => {
setShowModal(undefined);
const tx = await bond(data);
if (tx) {
setConfirmationDetails({
status: 'success',
title: 'Bonding successful',
txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`,
});
}
};
const handleMigrateVestedMixnode = async () => {
setShowMigrationModal(false);
const tx = await migrateVestedMixnode();
@@ -250,7 +263,7 @@ export const Bonding = () => {
/>
)}
<BondNymNodeModalWithState open={showModal === 'bond-nymnode'} onClose={handleCloseModal} />
<BondNymNode open={showModal === 'bond-nymnode'} onClose={handleCloseModal} onBond={handleBondNymNode} />
{showModal === 'update-bond-oversaturated' && uncappedSaturation && (
<BondOversaturatedModal
@@ -7,7 +7,6 @@ import { isMixnode, isNymNode } from 'src/types';
interface Props {
bondedNode: TBondedNode;
onConfirm: () => Promise<void>;
onError: (e: string) => void;
}
@@ -71,7 +70,7 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => {
</Stack>
</Grid>
</Grid>
{isConfirmed && !isNymNode(bondedNode) && (
{isConfirmed && (
<UnbondModal
node={bondedNode}
onConfirm={async () => {
@@ -3,22 +3,21 @@ import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { Box, Button, Divider, Grid, Stack, TextField, Typography } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { updateNymNodeConfig } from 'src/requests';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/legacyForms/mixnodeValidationSchema';
import { Console } from 'src/utils/console';
import { Alert } from 'src/components/Alert';
import { ConfirmTx } from 'src/components/ConfirmTX';
import { useGetFee } from 'src/hooks/useGetFee';
import { LoadingModal } from 'src/components/Modals/LoadingModal';
import { BalanceWarning } from 'src/components/FeeWarning';
import { AppContext } from 'src/context';
import { AppContext, useBondingContext } from 'src/context';
import { TBondedNymNode } from 'src/requests/nymNodeDetails';
import { settingsValidationSchema } from 'src/components/Bonding/forms/nym-node/settingsValidationSchema';
export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymNode }) => {
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
const { fee, resetFeeState } = useGetFee();
const { userBalance } = useContext(AppContext);
const { updateNymNodeConfig } = useBondingContext();
const theme = useTheme();
@@ -27,28 +26,26 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN
handleSubmit,
formState: { errors, isSubmitting, isDirty, isValid },
} = useForm({
resolver: yupResolver(bondedInfoParametersValidationSchema),
resolver: yupResolver(settingsValidationSchema),
mode: 'onChange',
defaultValues: {
host: bondedNode.host,
customHttpPort: bondedNode.customHttpPort,
custom_http_port: bondedNode.customHttpPort,
},
});
const onSubmit = async (data: { host?: string; customHttpPort?: number | null }) => {
const onSubmit = async ({ host, custom_http_port }: { host: string; custom_http_port: number | null }) => {
resetFeeState();
const { host, customHttpPort } = data;
if (host && customHttpPort) {
try {
const NymNodeConfigParams = {
host,
custom_http_port: customHttpPort,
custom_http_port,
};
try {
await updateNymNodeConfig(NymNodeConfigParams);
setOpenConfirmationModal(true);
} catch (error) {
Console.error(error);
}
await updateNymNodeConfig(NymNodeConfigParams);
setOpenConfirmationModal(true);
} catch (error) {
Console.error(error);
}
};
@@ -59,7 +56,7 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN
open
header="Update node settings"
fee={fee}
onConfirm={handleSubmit((d) => onSubmit(d))}
onConfirm={handleSubmit(onSubmit)}
onPrev={resetFeeState}
onClose={resetFeeState}
>
@@ -70,7 +67,6 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN
)}
</ConfirmTx>
)}
{isSubmitting && <LoadingModal />}
<Alert
title={
<Stack>
@@ -93,12 +89,12 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN
<Grid spacing={3} item container alignItems="center" xs={12} md={6}>
<Grid item width={1}>
<TextField
{...register('customHttpPort')}
name="customHttpPort"
{...register('custom_http_port')}
name="custom_http_port"
label="Custom HTTP port"
fullWidth
error={!!errors.customHttpPort}
helperText={errors.customHttpPort?.message}
error={!!errors.custom_http_port}
helperText={errors.custom_http_port?.message}
InputLabelProps={{ shrink: true }}
/>
</Grid>
@@ -134,7 +130,7 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN
size="large"
variant="contained"
disabled={isSubmitting || !isDirty || !isValid}
onClick={handleSubmit(() => undefined)}
onClick={handleSubmit(onSubmit)}
sx={{ m: 3, mr: 0 }}
fullWidth
>
@@ -152,7 +148,7 @@ export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymN
hideCloseIcon
displayInfoIcon
onOk={async () => {
await setOpenConfirmationModal(false);
setOpenConfirmationModal(false);
}}
buttonFullWidth
sx={{
+3 -3
View File
@@ -36,7 +36,7 @@ export type TBondNymNodeArgs = TNymNodeSignatureArgs & {
};
export type TNymNodeSignatureArgs = {
nymNode: NymNode;
nymnode: NymNode;
costParams: NodeCostParams;
pledge: DecCoin;
};
@@ -86,7 +86,7 @@ export type TNodeDescription = {
export type TNodeConfigUpdateArgs = {
host: string;
custom_http_port: number;
custom_http_port: number | null;
};
export type TDelegateArgs = {
@@ -110,7 +110,7 @@ export type TGatewayReport = {
most_recent: number;
};
export type TNodeRole = 'entry' | 'exit' | 'layer1' | 'layer2' | 'layer3' | 'standby';
export type TNodeRole = 'entryGateway' | 'exitGateway' | 'layer1' | 'layer2' | 'layer3' | 'standby';
export type MixnodeSaturationResponse = {
saturation: string;
@@ -21,7 +21,7 @@ impl MyTopologyProvider {
async fn get_topology(&self) -> NymTopology {
let mixnodes = self
.validator_client
.get_basic_active_mixing_assigned_nodes(None)
.get_all_basic_active_mixing_assigned_nodes(None)
.await
.unwrap();
+7 -1
View File
@@ -10,7 +10,13 @@ pub mod mixnet;
pub mod tcp_proxy;
pub use error::{Error, Result};
pub use nym_client_core::client::mix_traffic::transceiver::*;
pub use nym_client_core::client::{
mix_traffic::transceiver::*,
topology_control::{
GeoAwareTopologyProvider, NymApiTopologyProvider, NymApiTopologyProviderConfig,
TopologyProvider,
},
};
pub use nym_network_defaults::{
ChainDetails, DenomDetails, DenomDetailsOwned, NymContracts, NymNetworkDetails,
ValidatorDetails,
@@ -128,7 +128,9 @@ impl<S: Storage + Clone + 'static> Authenticator<S> {
// Connect to the mixnet
let mixnet_client = crate::mixnet_client::create_mixnet_client(
&self.config.base,
task_handle.get_handle().named("nym_sdk::MixnetClient"),
task_handle
.get_handle()
.named("nym_sdk::MixnetClient[AUTH]"),
self.custom_gateway_transceiver,
self.custom_topology_provider,
self.wait_for_gateway,
@@ -297,6 +297,10 @@ impl<S: Storage + Clone + 'static> MixnetListener<S> {
credential: CredentialSpendingData,
client_id: i64,
) -> Result<i64> {
ecash_verifier
.storage()
.create_bandwidth_entry(client_id)
.await?;
let bandwidth = ecash_verifier
.storage()
.get_available_bandwidth(client_id)
@@ -133,7 +133,7 @@ impl IpPacketRouter {
// Connect to the mixnet
let mixnet_client = crate::mixnet_client::create_mixnet_client(
&self.config.base,
task_handle.get_handle().named("nym_sdk::MixnetClient"),
task_handle.get_handle().named("nym_sdk::MixnetClient[IPR]"),
self.custom_gateway_transceiver,
self.custom_topology_provider,
self.wait_for_gateway,
@@ -239,7 +239,7 @@ impl NRServiceProviderBuilder {
// Connect to the mixnet
let mixnet_client = create_mixnet_client(
&self.config.base,
shutdown.get_handle().named("nym_sdk::MixnetClient"),
shutdown.get_handle().named("nym_sdk::MixnetClient[NR]"),
self.custom_gateway_transceiver,
self.custom_topology_provider,
self.wait_for_gateway,