Compare commits

...

55 Commits

Author SHA1 Message Date
Jon Häggblad cb929b251e Rename to set_active_gateway_if_previously_registered 2024-04-14 20:35:45 +02:00
Jon Häggblad b927acb35a Revert set_active_gateway in base_client 2024-04-14 20:35:45 +02:00
Jon Häggblad 9131a551d5 Downgrade log statement to debug 2024-04-14 20:35:45 +02:00
Jon Häggblad 41aa391d9d Make create_bandwidth_client pub 2024-04-14 20:35:45 +02:00
Jon Häggblad f44e17489d Remove pseudo builder methods 2024-04-14 20:35:45 +02:00
Jon Häggblad e3f893f4c4 Remove pub from internal setters 2024-04-14 20:35:45 +02:00
Jon Häggblad 2dffa07be1 Remove unused KeyMode type 2024-04-14 20:35:45 +02:00
Jon Häggblad 69e8b12328 Rework gateway setup in rust sdk mixnet client 2024-04-14 20:35:45 +02:00
Jon Häggblad 46b59cdb09 Add debug implementations for some gateway setup types 2024-04-14 20:35:44 +02:00
Jon Häggblad ad860303e8 Elevate log statements in gateway setup 2024-04-14 20:31:55 +02:00
Jon Häggblad 16e1a154e0 Set active gateway after setting up gateway in base client 2024-04-14 20:31:55 +02:00
Jędrzej Stuczyński 3e12766afd expose info whether entry gateway enforces zk-nym in self-described api (#4529) 2024-04-12 19:05:28 +02:00
Jędrzej Stuczyński 1c93cc8a68 Merge pull request #4437 from nymtech/feature/freepass-expiration
expire freepass bandwidth + stagger db flushes
2024-04-12 17:48:37 +01:00
Jędrzej Stuczyński 4865f7f205 [bugfix] Prevent generation of free passes with expiry date in the past (#4535) 2024-04-12 17:44:32 +02:00
Jędrzej Stuczyński a785950d76 [bugfix] don't shutdown NR on failed connection (#4539) 2024-04-12 17:43:22 +02:00
Jędrzej Stuczyński 328d1c25b7 removed old debug code 2024-04-12 15:23:37 +01:00
Jędrzej Stuczyński 862a248902 missing cli argument for controlling nyxd 2024-04-12 15:14:55 +01:00
Jędrzej Stuczyński f826904407 fix credential expiration + improve errors + naively stop client when out of bandwidth 2024-04-12 15:00:49 +01:00
Jędrzej Stuczyński 67467ca76d renamed 'shared_state' to 'common_state' 2024-04-12 15:00:49 +01:00
Jędrzej Stuczyński a53ae5b6b5 using lock guard in dedicated scope 2024-04-12 15:00:49 +01:00
Jędrzej Stuczyński bfc495ef29 expire freepass bandwidth + stagger db flushes 2024-04-12 15:00:48 +01:00
Jędrzej Stuczyński 9d74c22f9b Merge pull request #4536 from nymtech/bugfix/localnet
[bugfix] make sure localnet script works in post nym-node era
2024-04-12 12:41:53 +01:00
import this f978552a3a [DOC]: NymVPN testing Guides update (#4528)
* simplify cli - comment redundant pages

* unite mac and linux cli guide to one page

* finalise cli guide

* syntax edit

* book built

* syntax edit

* final version - ready to review

* final version - ready to review
2024-04-11 12:09:58 +00:00
Jędrzej Stuczyński 5dcad5ffd4 [bugfix] make sure localnet script works in post nym-node era 2024-04-11 10:25:04 +01:00
Jędrzej Stuczyński 210269cb9c [bugfix] restore old serialisation of 'NymNodeDescription' (#4533) 2024-04-11 10:11:46 +02:00
Jędrzej Stuczyński 8c59106add introduced /api-status routes for nym-api (#4527)
* introduced /api-status routes for nym-api

* added additional information to the signer-information endpoint
2024-04-11 09:28:42 +02:00
Tommy Verrall 264771f8cf Merge pull request #4428 from nymtech/sachin/delegate_from_file
nym-cli: delegations from a file
2024-04-10 15:36:11 +01:00
Tommy Verrall 28d27eb84e Merge pull request #4517 from nymtech/dependabot/cargo/sdk/ffi/shared/eyre-0.6.12
Bump eyre from 0.6.11 to 0.6.12 in /sdk/ffi/shared
2024-04-10 11:16:27 +01:00
Tommy Verrall d57757584c Merge pull request #4518 from nymtech/dependabot/cargo/sdk/ffi/cpp/eyre-0.6.12
Bump eyre from 0.6.11 to 0.6.12 in /sdk/ffi/cpp
2024-04-10 10:35:25 +01:00
Tommy Verrall 6da00513a7 Merge pull request #4519 from nymtech/dependabot/cargo/sdk/ffi/go/eyre-0.6.12
Bump eyre from 0.6.11 to 0.6.12 in /sdk/ffi/go
2024-04-10 10:25:38 +01:00
dependabot[bot] 035faf70e4 Bump eyre from 0.6.11 to 0.6.12 in /sdk/ffi/cpp
Bumps [eyre](https://github.com/eyre-rs/eyre) from 0.6.11 to 0.6.12.
- [Commits](https://github.com/eyre-rs/eyre/commits)

---
updated-dependencies:
- dependency-name: eyre
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-10 09:02:46 +00:00
Tommy Verrall b238d108e6 Merge pull request #4520 from nymtech/dependabot/cargo/sdk/ffi/cpp/h2-0.3.26
Bump h2 from 0.3.24 to 0.3.26 in /sdk/ffi/cpp
2024-04-10 10:01:47 +01:00
dependabot[bot] 5581b15094 Bump eyre from 0.6.11 to 0.6.12 in /sdk/ffi/go
Bumps [eyre](https://github.com/eyre-rs/eyre) from 0.6.11 to 0.6.12.
- [Commits](https://github.com/eyre-rs/eyre/commits)

---
updated-dependencies:
- dependency-name: eyre
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-10 08:34:13 +00:00
Tommy Verrall af19c4ecfd Merge pull request #4521 from nymtech/dependabot/cargo/sdk/ffi/go/h2-0.3.26
Bump h2 from 0.3.24 to 0.3.26 in /sdk/ffi/go
2024-04-10 09:31:31 +01:00
Jędrzej Stuczyński 1855423981 Merge pull request #4531 from nymtech/bugfix/self-described-backwards-compat
Bugfix/self described backwards compat
2024-04-09 16:49:18 +01:00
benedetta davico 229561bab9 update sandbox .env (#4530) 2024-04-09 17:47:30 +02:00
Jędrzej Stuczyński 7d2044c177 Merge pull request #4532 from nymtech/bugfix/standalone-node-http-api
run http api by default
2024-04-09 16:47:20 +01:00
Jędrzej Stuczyński 7885d3c986 removed dead code
removed dead code
2024-04-09 16:36:05 +01:00
Jędrzej Stuczyński 78fb3c2293 run http api by default 2024-04-09 16:26:54 +01:00
Jędrzej Stuczyński 0ed43d2439 allow nym-api to understand signatures on legacy host information 2024-04-09 16:17:39 +01:00
Jędrzej Stuczyński 69c1a32392 error prefix 2024-04-09 16:16:39 +01:00
Sachin Kamath 30ffea19a1 docs: add credential signing steps for nym-api (#4523)
* docs: add credential signing steps for nym-api

* review comments
2024-04-09 10:04:24 +02:00
Jon Häggblad 9e3bd8588d Re-export TransmissionLane type (#4525) 2024-04-08 16:05:12 +02:00
import this ac7b821782 [DOC]: Feature: cmdrun scripts (#4514)
* initialise cmdrun script dir

* initialise api_targets_config

* initialise api_targets

* first version of argparser cli

* create a token api stats table

* add format function

* initiate nym_vpn command

* add github to get api url

* add github api to config

* create nym_vpn releases dataframe

* menu headers tweak
2024-04-08 09:13:31 +00:00
dependabot[bot] bb372fb35c Bump eyre from 0.6.11 to 0.6.12 in /sdk/ffi/shared
Bumps [eyre](https://github.com/eyre-rs/eyre) from 0.6.11 to 0.6.12.
- [Commits](https://github.com/eyre-rs/eyre/commits)

---
updated-dependencies:
- dependency-name: eyre
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-08 08:28:10 +00:00
Tommy Verrall 5f4220867a Merge pull request #4522 from nymtech/dependabot/cargo/sdk/ffi/shared/h2-0.3.26
Bump h2 from 0.3.24 to 0.3.26 in /sdk/ffi/shared
2024-04-08 09:27:12 +01:00
dependabot[bot] 3a6058be60 Bump h2 from 0.3.24 to 0.3.26 in /sdk/ffi/shared
Bumps [h2](https://github.com/hyperium/h2) from 0.3.24 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.24...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-05 16:24:23 +00:00
dependabot[bot] 7fcc8cdd19 Bump h2 from 0.3.24 to 0.3.26 in /sdk/ffi/go
Bumps [h2](https://github.com/hyperium/h2) from 0.3.24 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.24...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-05 16:24:03 +00:00
dependabot[bot] 098bd4eb3c Bump h2 from 0.3.24 to 0.3.26 in /sdk/ffi/cpp
Bumps [h2](https://github.com/hyperium/h2) from 0.3.24 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.24...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-05 16:18:54 +00:00
Sachin Kamath 1ebb0c7daa don't use bigdecimal 2024-03-18 20:28:55 +05:30
Sachin Kamath 377e06daab nym-cli: clippy fixes 2024-02-22 18:10:41 +05:30
Sachin Kamath b54d82a01a nym-cli: batch transactions 2024-02-22 17:55:35 +05:30
Sachin Kamath ad052ef498 fmt + clippy 2024-02-21 19:14:12 +05:30
Sachin Kamath a3bc4af8fe fmt + clippy 2024-02-21 19:12:02 +05:30
Sachin Kamath b6d57e2862 nym-cli: delegate to mixnodes from input csv file
nym-cli: delegate to mixnodes from input csv file
2024-02-21 18:44:15 +05:30
80 changed files with 2132 additions and 501 deletions
Generated
+3
View File
@@ -5756,7 +5756,9 @@ dependencies = [
"nym-validator-client",
"rand 0.7.3",
"serde",
"si-scale",
"thiserror",
"time",
"tokio",
"tokio-stream",
"tokio-tungstenite",
@@ -6247,6 +6249,7 @@ dependencies = [
"nym-exit-policy",
"nym-http-api-client",
"nym-wireguard-types",
"rand_chacha 0.2.2",
"schemars",
"serde",
"serde_json",
@@ -8,5 +8,7 @@ use nym_client_core::cli_helpers::client_import_credential::{
};
pub(crate) async fn execute(args: CommonClientImportCredentialArgs) -> Result<(), ClientError> {
import_credential::<CliNativeClient, _>(args).await
import_credential::<CliNativeClient, _>(args).await?;
println!("successfully imported credential!");
Ok(())
}
@@ -10,5 +10,7 @@ use nym_client_core::cli_helpers::client_import_credential::{
pub(crate) async fn execute(
args: CommonClientImportCredentialArgs,
) -> Result<(), Socks5ClientError> {
import_credential::<CliSocks5Client, _>(args).await
import_credential::<CliSocks5Client, _>(args).await?;
println!("successfully imported credential!");
Ok(())
}
+1
View File
@@ -18,6 +18,7 @@ pub mod acquire;
pub mod error;
mod utils;
#[derive(Debug)]
pub struct BandwidthController<C, St> {
storage: St,
client: C,
+3 -3
View File
@@ -201,7 +201,7 @@ where
log::debug!("Setting up gateway");
match setup {
GatewaySetup::MustLoad { gateway_id } => {
log::trace!("GatewaySetup::MustLoad with id: {gateway_id:?}");
log::debug!("GatewaySetup::MustLoad with id: {gateway_id:?}");
use_loaded_gateway_details(key_store, details_store, gateway_id).await
}
GatewaySetup::New {
@@ -209,7 +209,7 @@ where
available_gateways,
wg_tun_address,
} => {
log::trace!("GatewaySetup::New with spec: {specification:?}");
log::debug!("GatewaySetup::New with spec: {specification:?}");
setup_new_gateway(
key_store,
details_store,
@@ -224,7 +224,7 @@ where
gateway_details,
client_keys: managed_keys,
} => {
log::trace!("GatewaySetup::ReuseConnection");
log::debug!("GatewaySetup::ReuseConnection");
Ok(reuse_gateway_connection(
authenticated_ephemeral_client,
*gateway_details,
@@ -16,6 +16,8 @@ thiserror = { workspace = true }
url = { workspace = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
tokio = { version = "1.24.1", features = ["macros"] }
si-scale = "0.2.2"
time.workspace = true
# internal
nym-bandwidth-controller = { path = "../../bandwidth-controller" }
@@ -79,6 +79,7 @@ impl GatewayConfig {
}
// TODO: this should be refactored into a state machine that keeps track of its authentication state
#[derive(Debug)]
pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
authenticated: bool,
disabled_credentials_mode: bool,
@@ -849,6 +850,7 @@ impl<C, St> GatewayClient<C, St> {
// type alias for an ease of use
pub type InitGatewayClient = GatewayClient<InitOnly>;
#[derive(Debug)]
pub struct InitOnly;
impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
@@ -10,9 +10,14 @@ use futures::stream::{SplitSink, SplitStream};
use futures::{SinkExt, StreamExt};
use log::*;
use nym_gateway_requests::registration::handshake::SharedKeys;
use nym_gateway_requests::ServerResponse;
use nym_task::TaskClient;
use si_scale::helpers::bibytes2;
use std::os::raw::c_int as RawFd;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tungstenite::Message;
#[cfg(unix)]
@@ -48,6 +53,22 @@ pub(crate) fn ws_fd(_conn: &WsConn) -> Option<RawFd> {
None
}
// disgusting? absolutely, but does the trick for now
static LAST_LOGGED_BANDWIDTH_TS: AtomicI64 = AtomicI64::new(0);
fn maybe_log_bandwidth(remaining: i64) {
// SAFETY: this value is always populated with valid timestamps
let last =
OffsetDateTime::from_unix_timestamp(LAST_LOGGED_BANDWIDTH_TS.load(Ordering::Relaxed))
.unwrap();
let now = OffsetDateTime::now_utc();
if last + Duration::from_secs(10) < now {
log::info!("remaining bandwidth: {}", bibytes2(remaining as f64));
LAST_LOGGED_BANDWIDTH_TS.store(now.unix_timestamp(), Ordering::Relaxed)
}
}
#[derive(Debug)]
pub(crate) struct PartiallyDelegated {
sink_half: SplitSink<WsConn, Message>,
delegated_stream: (SplitStreamReceiver, oneshot::Sender<()>),
@@ -55,7 +76,10 @@ pub(crate) struct PartiallyDelegated {
}
impl PartiallyDelegated {
fn recover_received_plaintexts(ws_msgs: Vec<Message>, shared_key: &SharedKeys) -> Vec<Vec<u8>> {
fn recover_received_plaintexts(
ws_msgs: Vec<Message>,
shared_key: &SharedKeys,
) -> Result<Vec<Vec<u8>>, GatewayClientError> {
let mut plaintexts = Vec::with_capacity(ws_msgs.len());
for ws_msg in ws_msgs {
match ws_msg {
@@ -73,15 +97,32 @@ impl PartiallyDelegated {
// TODO: those can return the "send confirmations" - perhaps it should be somehow worked around?
Message::Text(text) => {
trace!(
"received a text message - probably a response to some previous query! - {}",
text
"received a text message - probably a response to some previous query! - {text}",
);
match ServerResponse::try_from(text)
.map_err(|_| GatewayClientError::MalformedResponse)?
{
ServerResponse::Send {
remaining_bandwidth,
} => maybe_log_bandwidth(remaining_bandwidth),
ServerResponse::Error { message } => {
error!("gateway failure: {message}");
return Err(GatewayClientError::GatewayError(message));
}
other => {
warn!(
"received illegal message of type {} in an authenticated client",
other.name()
)
}
}
continue;
}
_ => continue,
}
}
plaintexts
Ok(plaintexts)
}
fn route_socket_messages(
@@ -89,7 +130,7 @@ impl PartiallyDelegated {
packet_router: &PacketRouter,
shared_key: &SharedKeys,
) -> Result<(), GatewayClientError> {
let plaintexts = Self::recover_received_plaintexts(ws_msgs, shared_key);
let plaintexts = Self::recover_received_plaintexts(ws_msgs, shared_key)?;
packet_router.route_received(plaintexts)
}
@@ -129,7 +170,8 @@ impl PartiallyDelegated {
};
if let Err(err) = Self::route_socket_messages(ws_msgs, &packet_router, shared_key.as_ref()) {
log::warn!("Route socket messages failed: {err}");
log::error!("Route socket messages failed: {err}");
break Err(err)
}
}
};
@@ -221,6 +263,7 @@ impl PartiallyDelegated {
// we can either have the stream itself or an option to re-obtain it
// by notifying the future owning it to finish the execution and awaiting the result
// which should be almost immediate (or an invalid state which should never, ever happen)
#[derive(Debug)]
pub(crate) enum SocketState {
Available(Box<WsConn>),
PartiallyDelegated(PartiallyDelegated),
@@ -148,6 +148,10 @@ pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> {
bail!("the provided free pass request has too long expiry (expiry is set to on {expiration_date})")
}
if expiration_date < now {
bail!("the provided free pass expiry is set in the past!")
}
// issuance start
block_until_coconut_is_available(&client).await?;
@@ -0,0 +1,354 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::fs::OpenOptions;
use clap::Parser;
use comfy_table::Table;
use csv::WriterBuilder;
use log::info;
use nym_mixnet_contract_common::ExecuteMsg;
use nym_mixnet_contract_common::ExecuteMsg::{DelegateToMixnode, UndelegateFromMixnode};
use nym_mixnet_contract_common::PendingEpochEventKind::{Delegate, Undelegate};
use nym_validator_client::nyxd::contract_traits::{NymContractsProvider, PagedMixnetQueryClient};
use nym_validator_client::nyxd::Coin;
use crate::context::SigningClient;
use crate::utils::pretty_coin;
#[derive(Debug, Parser)]
pub struct Args {
#[clap(long)]
pub memo: Option<String>,
#[clap(
long,
help = "Input csv files with delegation amounts. Format: (mixID, amount(in NYM))"
)]
pub input: String,
#[clap(
long,
help = "An output file path (CSV format) to create or append a log of results to"
)]
pub output: Option<String>,
}
#[derive(Debug)]
pub struct InputFileRow {
pub mix_id: String,
pub amount: Coin,
}
#[derive(Debug)]
pub struct InputFileReader {
pub rows: Vec<InputFileRow>,
}
impl InputFileReader {
pub fn new(path: &str) -> Result<InputFileReader, anyhow::Error> {
let file_contents = fs::read_to_string(path)?;
let mut rows = Vec::new();
let mut mix_id_set = HashSet::new();
for line in file_contents
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
{
let tokens: Vec<_> = line.split(',').collect();
if tokens.len() != 2 {
anyhow::bail!("Incorrect format: {}", line);
}
let mix_id = tokens[0].trim().to_string();
let input_amount = tokens[1]
.trim()
.parse::<u128>()
.map_err(|_| anyhow::anyhow!("'{}' has an invalid amount", line))?;
let micro_nym_amount = input_amount * 1_000_000;
if !mix_id_set.insert(mix_id.clone()) {
anyhow::bail!("Duplicate mix_id found: {}", mix_id);
}
rows.push(InputFileRow {
mix_id,
amount: Coin {
amount: micro_nym_amount,
denom: "unym".to_string(),
},
});
}
Ok(InputFileReader { rows })
}
}
fn write_to_csv(
output_details: Vec<[String; 3]>,
output_file: Option<String>,
) -> Result<(), anyhow::Error> {
if let Some(file_path) = output_file {
// Determine if the file exists and is not empty
let file_exists = fs::metadata(&file_path)
.map(|metadata| metadata.len() > 0)
.unwrap_or(false);
// Open the file for appending or creation
let file = OpenOptions::new()
.append(true)
.create(true)
.open(&file_path)?;
if !file_exists {
let mut wtr = csv::Writer::from_writer(&file);
wtr.write_record(["Operation", "Transaction Hash", "Timestamp"])?;
wtr.flush()?;
}
let mut wtr = WriterBuilder::new()
.has_headers(!file_exists)
.from_writer(file);
// Write the details to the CSV file
for detail in output_details {
wtr.write_record(&detail)?;
}
wtr.flush()?;
info!("All operations saved to output file");
}
Ok(())
}
async fn fetch_delegation_data(
client: &SigningClient,
) -> Result<HashMap<String, Coin>, anyhow::Error> {
let address = client.address();
// Fetch all delegations for the user
let delegations = match client.get_all_delegator_delegations(&address).await {
Ok(delegations) => delegations,
Err(e) => {
anyhow::bail!("Error fetching delegations: {}", e)
}
};
// Build a map to make it easier to handle delegation data
let mut existing_delegation_map: HashMap<String, Coin> = HashMap::new();
let mut pending_delegation_map: HashMap<String, Coin> = HashMap::new();
for delegation in delegations {
existing_delegation_map
.insert(delegation.mix_id.to_string(), Coin::from(delegation.amount));
}
// Look for pending delegate / undelegate events which might be of interest to us
let pending_events = match client.get_all_pending_epoch_events().await {
Ok(events) => events,
Err(e) => {
anyhow::bail!("Error fetching pending epoch events: {}", e);
}
};
for event in pending_events {
match event.event.kind {
// If a pending undelegate tx is found, remove it from delegation map
Undelegate { owner, mix_id, .. } => {
if owner == address.as_ref()
&& existing_delegation_map.get(&mix_id.to_string()).is_some()
{
existing_delegation_map.remove(&mix_id.to_string());
}
}
// If a pending delegation event is found, gather them to consolidate later
Delegate {
owner,
mix_id,
amount,
..
} => {
if owner == address.as_ref() {
let mut amount = Coin::from(amount);
if let Some(pending_record) = pending_delegation_map.get(&mix_id.to_string()) {
amount.amount += pending_record.amount;
}
pending_delegation_map.insert(mix_id.to_string(), amount);
}
}
_ => {}
};
}
// Consolidate pending events into delegation map
for (mix_id, amount) in pending_delegation_map {
existing_delegation_map
.entry(mix_id)
.and_modify(|e| e.amount += amount.amount)
.or_insert(amount);
}
Ok(existing_delegation_map)
}
pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) {
let records = match InputFileReader::new(&args.input) {
Ok(records) => records,
Err(e) => {
println!("Error reading input file: {}", e);
return;
}
};
let existing_delegation_map = fetch_delegation_data(&client)
.await
.expect("Could not fetch existing delegations");
let mut delegation_table = Table::new();
let mut undelegation_table = Table::new();
let mut delegation_msgs: Vec<(ExecuteMsg, Vec<Coin>)> = Vec::new();
let mut undelegation_msgs: Vec<(ExecuteMsg, Vec<Coin>)> = Vec::new();
delegation_table.set_header(["Mix ID", "Input Amount", "Adjusted Amount"]);
undelegation_table.set_header(["Mix ID"]);
for row in &records.rows {
let input_amount = row.amount.amount;
let existing_delegation_amount = existing_delegation_map
.get(&row.mix_id)
.map_or(0, |coin| coin.amount);
match existing_delegation_amount.cmp(&input_amount) {
Ordering::Equal => continue, // No action needed if amounts are equal
Ordering::Less => {
// Delegate the difference if the existing delegation is less
let difference = Coin {
amount: input_amount - existing_delegation_amount,
denom: row.amount.denom.clone(),
};
let mix_id = row.mix_id.clone().parse::<u32>().unwrap();
delegation_msgs.push((DelegateToMixnode { mix_id }, vec![difference.clone()]));
delegation_table.add_row(&[
row.mix_id.clone(),
pretty_coin(&row.amount),
pretty_coin(&difference),
]);
}
Ordering::Greater => {
let mix_id = row.mix_id.clone().parse::<u32>().unwrap();
let coins: Vec<Coin> = vec![];
undelegation_msgs.push((UndelegateFromMixnode { mix_id }, coins));
undelegation_table.add_row(&[row.mix_id.clone()]);
if row.amount.amount > 0 {
delegation_msgs.push((DelegateToMixnode { mix_id }, vec![row.amount.clone()]));
delegation_table.add_row(&[
row.mix_id.clone(),
pretty_coin(&row.amount),
pretty_coin(&row.amount),
]);
}
}
}
}
if delegation_msgs.is_empty() && undelegation_msgs.is_empty() {
println!("Nothing to do. Delegations are up-to-date!");
return;
}
if !undelegation_msgs.is_empty() {
println!("Undelegation records : \n{}\n\n", undelegation_table);
}
if !delegation_msgs.is_empty() {
println!("Delegation records : \n{}\n\n", delegation_table);
}
let ans = inquire::Confirm::new("Do you want to continue with the shown operations?")
.with_default(false)
.with_help_message("You must confirm before the transactions are signed")
.prompt();
if let Err(e) = ans {
info!("Aborting, {}...", e);
return;
}
if let Ok(false) = ans {
info!("Aborting:: User denied proceeding with signing!");
return;
}
let mut output_details: Vec<[String; 3]> = Vec::new();
let now = time::OffsetDateTime::now_utc();
let now = now
.format(&time::format_description::well_known::Rfc3339)
.unwrap();
let mixnet_contract = client
.mixnet_contract_address()
.expect("mixnet contract address is not available");
// Execute all undelegation transactions
if !undelegation_msgs.is_empty() {
let res = client
.execute_multiple(
mixnet_contract,
undelegation_msgs.clone(),
None,
format!(
"Undelegate from {} nodes via nym-cli",
undelegation_msgs.len()
),
)
.await
.expect("Could not undelegate!");
println!(
"Undelegation transaction successful : {}",
res.transaction_hash
);
output_details.push([
"Undelegate".to_string(),
res.transaction_hash.to_string(),
now.clone(),
]);
}
// Execute all delegation delegations
if !delegation_msgs.is_empty() {
let res = client
.execute_multiple(
mixnet_contract,
delegation_msgs,
None,
format!(
"Delegatation to {} nodes via nym-cli",
undelegation_msgs.len()
),
)
.await
.expect("Could not delegate");
println!(
"Delegation transaction successful : {}",
res.transaction_hash
);
output_details.push([
"Delegate".to_string(),
res.transaction_hash.to_string(),
now.clone(),
]);
}
if args.output.is_some() {
if let Err(e) = write_to_csv(output_details, args.output) {
info!("Failed to write to CSV, {}", e);
}
}
}
@@ -6,6 +6,7 @@ use clap::{Args, Subcommand};
pub mod rewards;
pub mod delegate_to_mixnode;
pub mod delegate_to_multiple_mixnodes;
pub mod query_for_delegations;
pub mod undelegate_from_mixnode;
pub mod vesting_delegate_to_mixnode;
@@ -26,6 +27,8 @@ pub enum MixnetDelegatorsCommands {
Rewards(rewards::MixnetDelegatorsReward),
/// Delegate to a mixnode
Delegate(delegate_to_mixnode::Args),
/// Perform bulk delegations from an input file
DelegateMulti(delegate_to_multiple_mixnodes::Args),
/// Undelegate from a mixnode
Undelegate(undelegate_from_mixnode::Args),
/// Delegate to a mixnode with locked tokens
@@ -0,0 +1,48 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
ALTER TABLE coconut_credentials
RENAME TO old_coconut_credentials;
CREATE TABLE coconut_credentials
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-- introduce a way for us to introduce breaking changes in serialization
serialization_revision INTEGER NOT NULL,
-- the best we can do without enums
credential_type TEXT CHECK ( credential_type IN ('BandwidthVoucher', 'FreeBandwidthPass') ) NOT NULL,
credential_data BLOB NOT NULL UNIQUE,
epoch_id INTEGER NOT NULL,
-- this field is only really applicable to free passes
expired BOOLEAN NOT NULL
);
ALTER TABLE credential_usage
RENAME TO old_credential_usage;
-- for bandwidth vouchers there's going to be only a single entry; for freepasses there can be as many as there are gateways
CREATE TABLE credential_usage
(
credential_id INTEGER NOT NULL REFERENCES coconut_credentials (id),
gateway_id_bs58 TEXT NOT NULL,
-- no matter credential type, we can't spend the same credential with the same gateway multiple times
UNIQUE (credential_id, gateway_id_bs58)
);
INSERT INTO coconut_credentials
SELECT *
FROM old_coconut_credentials;
INSERT INTO credential_usage
SELECT *
FROM old_credential_usage;
DROP TABLE old_coconut_credentials;
DROP TABLE old_credential_usage;
@@ -44,7 +44,7 @@ impl CoconutCredentialManager {
r#"
SELECT *
FROM coconut_credentials
WHERE coconut_credentials.credential_type == "FreeBandwidthPass"
WHERE coconut_credentials.credential_type == "FreeBandwidthPass" AND coconut_credentials.expired = false
AND NOT EXISTS (SELECT 1
FROM credential_usage
WHERE credential_usage.credential_id = coconut_credentials.id
@@ -1,6 +1,8 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::{self, Debug, Formatter};
use crate::backends::memory::CoconutCredentialManager;
use crate::error::StorageError;
use crate::models::{StorableIssuedCredential, StoredIssuedCredential};
@@ -23,6 +25,12 @@ impl Default for EphemeralStorage {
}
}
impl Debug for EphemeralStorage {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "EphemeralStorage")
}
}
#[async_trait]
impl Storage for EphemeralStorage {
type StorageError = StorageError;
+4
View File
@@ -474,6 +474,10 @@ impl TaskClient {
self.mode.set_should_not_signal_on_drop();
}
pub fn disarm(&mut self) {
self.mark_as_success();
}
pub fn send_we_stopped(&mut self, err: SentError) {
if self.mode.is_dummy() {
return;
+2 -2
View File
@@ -205,10 +205,10 @@ impl<'a> TryFrom<&'a DescribedGateway> for Node {
clients_ws_port: self_described.mixnet_websockets.ws_port,
clients_wss_port: self_described.mixnet_websockets.wss_port,
identity_key: identity::PublicKey::from_base58_string(
&self_described.host_information.keys.ed25519_identity,
&self_described.host_information.keys.ed25519,
)?,
sphinx_key: encryption::PublicKey::from_base58_string(
&self_described.host_information.keys.x25519_sphinx,
&self_described.host_information.keys.x25519,
)?,
version: self_described
.build_information
+7
View File
@@ -6,6 +6,7 @@ use futures::{Sink, Stream};
use gloo_net::websocket::futures::WebSocket;
use gloo_net::websocket::{Message, WebSocketError};
use gloo_utils::errors::JsError;
use std::fmt::{self, Formatter};
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
@@ -77,6 +78,12 @@ impl Stream for JSWebsocket {
}
}
impl fmt::Debug for JSWebsocket {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "JSWebSocket")
}
}
impl Sink<WsMessage> for JSWebsocket {
type Error = WsError;
+1 -2
View File
@@ -1,3 +1,4 @@
#
# Summary
- [Introduction](introduction.md)
@@ -23,8 +24,6 @@
- [Linux](nymvpn/gui-linux.md)
- [MacOS](nymvpn/gui-mac.md)
- [CLI](nymvpn/cli.md)
- [Linux](nymvpn/cli-linux.md)
- [MacOS](nymvpn/cli-mac.md)
- [Troubleshooting](nymvpn/troubleshooting.md)
- [NymVPN FAQ](nymvpn/faq.md)
- [NymConnect X Monero](tutorials/monero.md)
@@ -1,4 +1,4 @@
# NymVPN alpha CLI: Guide for GNU/Linux
# NymVPN alpha CLI Guide
```admonish info
NymVPN is an experimental software and it's for testing purposes only. All users testing the client are expected to sign GDPR Information Sheet and Consent Form (shared at the workshop) so we use their results to improve the client, and submit the form [*NymVPN User research*]({{nym_vpn_form_url}}) with the testing results.
@@ -8,7 +8,7 @@ NymVPN is an experimental software and it's for testing purposes only. All users
> Any syntax in `<>` brackets is a user's/version unique variable. Exchange with a corresponding name without the `<>` brackets.
1. Open Github [releases page]({{nym_vpn_releases}}) and download the binary for Debian based Linux
1. Open Github [releases page]({{nym_vpn_releases}}) and download the CLI latest binary for your system
2. Verify sha hash of your downloaded binary with the one listed on the [releases page]({{nym_vpn_releases}}). You can use a simple `shasum` command and compare strings (ie with Python) or run in the same directory the following command, exchanging `<SHA_STRING>` with the one of your binary, like in the example:
```sh
@@ -25,17 +25,12 @@ tar -xvf <BINARY>.tar.gz
# tar -xvf nym-vpn-cli_<!-- cmdrun scripts/nym_vpn_cli_version.sh -->_ubuntu-22.04_x86_64.tar.gz
```
4. Make executable by running:
4. Make executable:
```sh
# make sure you are in the right sub-directory
chmod u+x ./nym-vpn-cli
```
5. Create Sandbox environment config file by saving [this](https://raw.githubusercontent.com/nymtech/nym/develop/envs/sandbox.env) as `sandbox.env` in the same directory as your NymVPN binaries by running:
```sh
curl -o sandbox.env -L https://raw.githubusercontent.com/nymtech/nym/develop/envs/sandbox.env
```
## Run NymVPN
**For NymVPN to work, all other VPNs must be switched off!** At this alpha stage of NymVPN, the network connection (wifi) must be reconnected after or in between the testing rounds.
@@ -47,7 +42,7 @@ Make sure your terminal is open in the same directory as your `nym-vpn-cli` bina
```sh
sudo ./nym-vpn-cli -c ./sandbox.env --entry-gateway-id <ENTRY_GATEWAY_ID> --exit-router-address <EXIT_ROUTER_ADDRESS> --enable-wireguard --private-key <PRIVATE_KEY> --wg-ip <WIREGUARD_IP>
```
3. To choose different Gateways, visit [nymvpn.com/en/alpha/api/gateways](https://nymvpn.com/en/alpha/api/gateways) and pick one
3. To choose different Gateways, visit [explorer.nymtech.net/network-components/gateways](https://explorer.nymtech.net/network-components/gateways) and copy-paste an identity key of your choice
4. See all possibilities in [command explanation](#cli-commands-and-options) section below
In case of errors, see [troubleshooting section](troubleshooting.md).
@@ -56,16 +51,16 @@ In case of errors, see [troubleshooting section](troubleshooting.md).
The basic syntax of `nym-vpn-cli` is:
```sh
sudo ./nym-vpn-cli -c ./sandbox.env --entry-gateway-id <ENTRY_GATEWAY_ID> --exit-router-address <EXIT_ROUTER_ADDRESS> --enable-wireguard --private-key <PRIVATE_KEY> --wg-ip <WG_IP>
sudo ./nym-vpn-cli <--exit-router-address <EXIT_ROUTER_ADDRESS>|--exit-gateway-id <EXIT_GATEWAY_ID>|--exit-gateway-country <EXIT_GATEWAY_COUNTRY>>
```
* To choose different Gateways, visit [nymvpn.com/en/alpha/api/gateways](https://nymvpn.com/en/alpha/api/gateways)
* To choose different Gateways, visit [nymvpn.com/en/alpha/api/gateways](https://explorer.nymtech.net/network-components/gateways)
* To see all possibilities run with `--help` flag:
```sh
./nym-vpn-cli --help
```
~~~admonish example collapsible=true title="Console output"
```sh
Usage: nym-vpn-cli [OPTIONS]
Usage: nym-vpn-cli [OPTIONS] <--exit-router-address <EXIT_ROUTER_ADDRESS>|--exit-gateway-id <EXIT_GATEWAY_ID>|--exit-gateway-country <EXIT_GATEWAY_COUNTRY>>
Options:
-c, --config-env-file <CONFIG_ENV_FILE>
@@ -76,11 +71,13 @@ Options:
Mixnet public ID of the entry gateway
--entry-gateway-country <ENTRY_GATEWAY_COUNTRY>
Auto-select entry gateway by country ISO
--entry-gateway-low-latency
Auto-select entry gateway by latency
--exit-router-address <EXIT_ROUTER_ADDRESS>
Mixnet recipient address
--exit-gateway-id <EXIT_GATEWAY_ID>
--exit-router-country <EXIT_ROUTER_COUNTRY>
--exit-gateway-country <EXIT_GATEWAY_COUNTRY>
Mixnet recipient address
--enable-wireguard
Enable the wireguard traffic between the client and the entry gateway
@@ -88,8 +85,10 @@ Options:
Associated private key
--wg-ip <WG_IP>
The IP address of the wireguard interface used for the first hop to the entry gateway
--nym-ip <NYM_IP>
The IP address of the nym TUN device that wraps IP packets in sphinx packets
--nym-ipv4 <NYM_IPV4>
The IPv4 address of the nym TUN device that wraps IP packets in sphinx packets
--nym-ipv6 <NYM_IPV6>
The IPv6 address of the nym TUN device that wraps IP packets in sphinx packets
--nym-mtu <NYM_MTU>
The MTU of the nym TUN device that wraps IP packets in sphinx packets
--disable-routing
@@ -125,3 +124,19 @@ Here is a list of the options and their descriptions. Some are essential, some a
- `--ip` is the IP address of the TUN device. That is the IP address of the local private network that is set up between local client and the Exit Gateway.
- `--mtu`: The MTU of the TUN device. That is the max IP packet size of the local private network that is set up between local client and the Exit Gateway.
- `--disable-routing`: Disable routing all traffic through the VPN TUN device.
## Testnet environment
If you want to run NymVPN CLI in Nym Sandbox environment, there are a few adjustments to be done:
1. Create Sandbox environment config file by saving [this](https://raw.githubusercontent.com/nymtech/nym/develop/envs/sandbox.env) as `sandbox.env` in the same directory as your NymVPN binaries by running:
```sh
curl -o sandbox.env -L https://raw.githubusercontent.com/nymtech/nym/develop/envs/sandbox.env
```
1. Check available Gateways at [nymvpn.com/en/alpha/api/gateways](https://nymvpn.com/en/alpha/api/gateways)
2. Run with a flag `-c`
```sh
sudo ./nym-vpn-cli -c <PATH_TO>/sandbox.env <--exit-router-address <EXIT_ROUTER_ADDRESS>|--exit-gateway-id <EXIT_GATEWAY_ID>|--exit-gateway-country <EXIT_GATEWAY_COUNTRY>>
```
@@ -0,0 +1,35 @@
# NymVPN Command Line Interface (CLI)
```admonish info
Our alpha testing round is done with participants at live workshop events. This guide will not work for everyone, as the NymVPN source code is not yet publicly accessible. The alpha testing is done on Nym testnet Sandbox environment, this configuration is limited and will not work in the future.
**If you commit to test NymVPN alpha, please start with the [user research form]({{nym_vpn_form_url}}) where all the steps will be provided**. If you disagree with any of the conditions listed, please leave this page.
```
Follow the simple [automated script](#automated-script-for-cli-installation) below to install and run NymVPN CLI. If you prefer to do a manual setup follow the steps in the guide for [Linux](cli-linux.md) or [MacOS](cli-mac.md).
Visit NymVPN alpha latest [release page]({{nym_vpn_releases}}) to check sha sums or download the binaries directly.
## Automated Script for CLI Installation
We wrote a [script](https://gist.github.com/serinko/d65450653d6bbafacbcee71c9cb8fb31) which does download of the CLI, sha256 verification, extraction, installation and configuration for Linux and MacOS users automatically following the steps below:
1. Open a terminal window in a directory where you want the script and NymVPN CLI binary be downloaded and run
```sh
curl -o execute-nym-vpn-cli-binary.sh -L https://gist.githubusercontent.com/tommyv1987/87267ded27e1eb7651aa9cc745ddf4af/raw/d39f98dbb36ccff761a7e940073388a6fe7b73fe/execute-nym-vpn-cli-binary.sh && chmod u+x execute-nym-vpn-cli-binary.sh && sudo -E ./execute-nym-vpn-cli-binary.sh
```
2. Follow the prompts in the program
3. The script will automatically start the client. Make sure to **turn off any other VPNs** and follow the prompts:
* It prints a JSON view of existing Gateways and prompt you to:
- *Make sure to use two different Gateways for entry and exit!*
- `enter a gateway ID:` paste one of the values labeled with a key `"identityKey"` printed above (without `" "`)
- `enter an exit address:` paste one of the values labeled with a key `"address"` printed above (without `" "`)
- `do you want five hop or two hop?`: type `five` or `two`
- `enable WireGuard? (yes/no):` if you chose yes, find your private key and wireguard IP [here](https://nymvpn.com/en/alpha)
To run `nym-vpn-cli` again, reconnect your wifi, move to the directory of your CLI binary `cd ~/nym-vpn-cli-dir` and follow the guide for [Linux](cli-linux.md#run-nymvpn) or [MacOS](cli-mac.md#run-nymvpn). If you find it too difficult, just run this script again - like in step \#3 above.
In case of errors check out the [troubleshooting](troubleshooting.md) section.
+137 -21
View File
@@ -1,35 +1,151 @@
# NymVPN Command Line Interface (CLI)
# NymVPN alpha CLI Guide
```admonish info
Our alpha testing round is done with participants at live workshop events. This guide will not work for everyone, as the NymVPN source code is not yet publicly accessible. The alpha testing is done on Nym testnet Sandbox environment, this configuration is limited and will not work in the future.
**If you commit to test NymVPN alpha, please start with the [user research form]({{nym_vpn_form_url}}) where all the steps will be provided**. If you disagree with any of the conditions listed, please leave this page.
NymVPN is an experimental software and it's for testing purposes only. All users testing the client are expected to sign GDPR Information Sheet and Consent Form (shared at the workshop) so we use their results to improve the client, and submit the form [*NymVPN User research*]({{nym_vpn_form_url}}) with the testing results.
```
Follow the simple [automated script](#automated-script-for-cli-installation) below to install and run NymVPN CLI. If you prefer to do a manual setup follow the steps in the guide for [Linux](cli-linux.md) or [MacOS](cli-mac.md).
## Installation
Visit NymVPN alpha latest [release page]({{nym_vpn_releases}}) to check sha sums or download the binaries directly.
> Any syntax in `<>` brackets is a user's/version unique variable. Exchange with a corresponding name without the `<>` brackets.
## Automated Script for CLI Installation
1. Open Github [releases page]({{nym_vpn_releases}}) and download the CLI latest binary for your system
We wrote a [script](https://gist.github.com/serinko/d65450653d6bbafacbcee71c9cb8fb31) which does download of the CLI, sha256 verification, extraction, installation and configuration for Linux and MacOS users automatically following the steps below:
1. Open a terminal window in a directory where you want the script and NymVPN CLI binary be downloaded and run
2. Verify sha hash of your downloaded binary with the one listed on the [releases page]({{nym_vpn_releases}}). You can use a simple `shasum` command and compare strings (ie with Python) or run in the same directory the following command, exchanging `<SHA_STRING>` with the one of your binary, like in the example:
```sh
curl -o execute-nym-vpn-cli-binary.sh -L https://gist.githubusercontent.com/tommyv1987/87267ded27e1eb7651aa9cc745ddf4af/raw/d39f98dbb36ccff761a7e940073388a6fe7b73fe/execute-nym-vpn-cli-binary.sh && chmod u+x execute-nym-vpn-cli-binary.sh && sudo -E ./execute-nym-vpn-cli-binary.sh
echo "<SHA_STRING>" | shasum -a 256 -c
# choose a correct one according to your binary, this is just an example
# echo "0e4abb461e86b2c168577e0294112a3bacd3a24bf8565b49783bfebd9b530e23 nym-vpn-cli_<!-- cmdrun scripts/nym_vpn_cli_version.sh -->_ubuntu-22.04_amd64.tar.gz" | shasum -a 256 -c
```
2. Follow the prompts in the program
3. Extract files:
```sh
tar -xvf <BINARY>.tar.gz
# for example
# tar -xvf nym-vpn-cli_<!-- cmdrun scripts/nym_vpn_cli_version.sh -->_ubuntu-22.04_x86_64.tar.gz
```
3. The script will automatically start the client. Make sure to **turn off any other VPNs** and follow the prompts:
4. Make executable:
```sh
# make sure you are in the right sub-directory
chmod u+x nym-vpn-cli
```
* It prints a JSON view of existing Gateways and prompt you to:
- *Make sure to use two different Gateways for entry and exit!*
- `enter a gateway ID:` paste one of the values labeled with a key `"identityKey"` printed above (without `" "`)
- `enter an exit address:` paste one of the values labeled with a key `"address"` printed above (without `" "`)
- `do you want five hop or two hop?`: type `five` or `two`
- `enable WireGuard? (yes/no):` if you chose yes, find your private key and wireguard IP [here](https://nymvpn.com/en/alpha)
## Run NymVPN
To run `nym-vpn-cli` again, reconnect your wifi, move to the directory of your CLI binary `cd ~/nym-vpn-cli-dir` and follow the guide for [Linux](cli-linux.md#run-nymvpn) or [MacOS](cli-mac.md#run-nymvpn). If you find it too difficult, just run this script again - like in step \#3 above.
**For NymVPN to work, all other VPNs must be switched off!** At this alpha stage of NymVPN, the network connection (wifi) must be reconnected after or in between the testing rounds.
In case of errors check out the [troubleshooting](troubleshooting.md) section.
Make sure your terminal is open in the same directory as your `nym-vpn-cli` binary.
1. Run it as root with `sudo` - the command will look like this with specified arguments:
```sh
# choose only one conditional --argument listed in {brackets}
sudo ./nym-vpn-cli { --exit-router-address <EXIT_ROUTER_ADDRESS>|--exit-gateway-id <EXIT_GATEWAY_ID>|--exit-gateway-country <EXIT_GATEWAY_COUNTRY> }
```
2. To choose different Gateways, visit [explorer.nymtech.net/network-components/gateways](https://explorer.nymtech.net/network-components/gateways) and copy-paste an identity key of your choice
```admonish note
Nym Exit Gateway functionality was implemented just recently and not all the Gateways are upgraded and ready to handle the VPN connections. If you want to make sure you are connecting to a Gateway with an embedded Network Requester, IP Packet Router and applied Nym exit policy, visit [this page](https://nymtech.net/events/fast-and-furious), scroll down to the list and search Gateways with all the functionalities enabled.
```
3. See all possibilities in [command explanation](#cli-commands-and-options) section below
4. In case of errors, see [troubleshooting section](troubleshooting.md)
### CLI Commands and Options
The basic syntax of `nym-vpn-cli` is:
```sh
# choose only one conditional --argument listed in {brackets}
sudo ./nym-vpn-cli { --exit-router-address <EXIT_ROUTER_ADDRESS>|--exit-gateway-id <EXIT_GATEWAY_ID>|--exit-gateway-country <EXIT_GATEWAY_COUNTRY> }
```
To see all the possibilities run with `--help` flag:
```sh
./nym-vpn-cli --help
```
~~~admonish example collapsible=true title="Console output"
```sh
Usage: nym-vpn-cli [OPTIONS] <--exit-router-address <EXIT_ROUTER_ADDRESS>|--exit-gateway-id <EXIT_GATEWAY_ID>|--exit-gateway-country <EXIT_GATEWAY_COUNTRY>>
Options:
-c, --config-env-file <CONFIG_ENV_FILE>
Path pointing to an env file describing the network
--mixnet-client-path <MIXNET_CLIENT_PATH>
Path to the data directory of a previously initialised mixnet client, where the keys reside
--entry-gateway-id <ENTRY_GATEWAY_ID>
Mixnet public ID of the entry gateway
--entry-gateway-country <ENTRY_GATEWAY_COUNTRY>
Auto-select entry gateway by country ISO
--entry-gateway-low-latency
Auto-select entry gateway by latency
--exit-router-address <EXIT_ROUTER_ADDRESS>
Mixnet recipient address
--exit-gateway-id <EXIT_GATEWAY_ID>
--exit-gateway-country <EXIT_GATEWAY_COUNTRY>
Mixnet recipient address
--enable-wireguard
Enable the wireguard traffic between the client and the entry gateway
--private-key <PRIVATE_KEY>
Associated private key
--wg-ip <WG_IP>
The IP address of the wireguard interface used for the first hop to the entry gateway
--nym-ipv4 <NYM_IPV4>
The IPv4 address of the nym TUN device that wraps IP packets in sphinx packets
--nym-ipv6 <NYM_IPV6>
The IPv6 address of the nym TUN device that wraps IP packets in sphinx packets
--nym-mtu <NYM_MTU>
The MTU of the nym TUN device that wraps IP packets in sphinx packets
--disable-routing
Disable routing all traffic through the nym TUN device. When the flag is set, the nym TUN device will be created, but to route traffic through it you will need to do it manually, e.g. ping -Itun0
--enable-two-hop
Enable two-hop mixnet traffic. This means that traffic jumps directly from entry gateway to exit gateway
--enable-poisson-rate
Enable Poisson process rate limiting of outbound traffic
--disable-background-cover-traffic
Disable constant rate background loop cover traffic
-h, --help
Print help
-V, --version
Print version
```
~~~
Here is a list of the options and their descriptions. Some are essential, some are more technical and not needed to be adjusted by users.
**Fundamental commands and arguments**
- `--entry-gateway-id`: paste one of the values labeled with a key `"identityKey"` (without `" "`)
- `--exit-gateway-id`: paste one of the values labeled with a key `"identityKey"` (without `" "`)
- `--exit-router-address`: paste one of the values labeled with a key `"address"` (without `" "`)
- `--enable-wireguard`: Enable the wireguard traffic between the client and the entry gateway. NymVPN uses Mullvad libraries for wrapping `wireguard-go` and to setup local routing rules to route all traffic to the TUN virtual network device
- `--wg-ip`: The address of the wireguard interface, you can get it [here](https://nymvpn.com/en/alpha)
- `--private-key`: get your private key for testing purposes [here](https://nymvpn.com/en/alpha)
- `--enable-two-hop` is a faster setup where the traffic is routed from the client to Entry Gateway and directly to Exit Gateway (default is 5-hops)
**Advanced options**
- `-c` is a path to an enviroment config, like [`sandbox.env`](https://raw.githubusercontent.com/nymtech/nym/develop/envs/sandbox.env)
- `--enable-poisson`: Enables process rate limiting of outbound traffic (disabled by default). It means that NymVPN client will send packets at a steady stream to the Entry Gateway. By default it's on average one sphinx packet per 20ms, but there is some randomness (poisson distribution). When there are no real data to fill the sphinx packets with, cover packets are generated instead.
- `--ip` is the IP address of the TUN device. That is the IP address of the local private network that is set up between local client and the Exit Gateway.
- `--mtu`: The MTU of the TUN device. That is the max IP packet size of the local private network that is set up between local client and the Exit Gateway.
- `--disable-routing`: Disable routing all traffic through the VPN TUN device.
## Testnet environment
If you want to run NymVPN CLI in Nym Sandbox environment, there are a few adjustments to be done:
1. Create Sandbox environment config file by saving [this](https://raw.githubusercontent.com/nymtech/nym/develop/envs/sandbox.env) as `sandbox.env` in the same directory as your NymVPN binaries:
```sh
curl -o sandbox.env -L https://raw.githubusercontent.com/nymtech/nym/develop/envs/sandbox.env
```
1. Check available Gateways at [nymvpn.com/en/alpha/api/gateways](https://nymvpn.com/en/alpha/api/gateways)
2. Run with a flag `-c`
```sh
sudo ./nym-vpn-cli -c <PATH_TO>/sandbox.env <--exit-router-address <EXIT_ROUTER_ADDRESS>|--exit-gateway-id <EXIT_GATEWAY_ID>|--exit-gateway-country <EXIT_GATEWAY_COUNTRY>>
```
+4 -4
View File
@@ -1,12 +1,12 @@
# NymVPN - Desktop (GUI)
```admonish info
Our alpha testing round is done with participants at live workshop events. This guide will not work for everyone, as the NymVPN source code is not yet publicly accessible. The alpha testing is done on Nym testnet Sandbox environment, this configuration is limited and will not work in the future.
Our alpha testing round is done with participants at live workshop events and the application in this stage may not work for everyone.
**If you commit to test NymVPN alpha, please start with the [user research form]({{nym_vpn_form_url}}) where all the steps will be provided**. If you disagree with any of the conditions listed, please leave this page.
```
This is the alpha version of NymVPN desktop application (GUI). A demo of how the client will look like for majority of day-to-day users.
This is a desktop (GUI) version of NymVPN client. A demo of how the application will look like for majority of day-to-day users.
Follow the simple [automated script](#automated-script-for-gui-installation) below to install and run NymVPN GUI. If the script didn't work for your distribution or you prefer to do a manual setup follow the steps in the guide for [Linux](gui-linux.md) or [MacOS](gui-mac.md) .
@@ -14,12 +14,12 @@ Visit NymVPN alpha latest [release page]({{nym_vpn_releases}}) to check sha sums
## Linux AppImage Automated Installation Method
The latest releases contain `appimage.sh` script. This method makes the installation simple for Linux users who want to run NymVPN from AppImmage. Executing the command below will download the binary to `~/.local/bin` and verify the checksum.
The latest releases contain `appimage.sh` script. This method makes the installation simple for Linux users who want to run NymVPN from AppImmage. Executing the command below will download the binary to `~/.local/bin` and verify the checksum:
```sh
curl -fsSL https://github.com/nymtech/nym-vpn-client/releases/download/nym-vpn-desktop-v<!-- cmdrun scripts/nym_vpn_desktop_version.sh -->/appimage.sh | bash
```
Run with the command
Run with the command:
```sh
sudo -E ~/.local/bin/nym-vpn.appimage
```
+3 -4
View File
@@ -5,16 +5,15 @@
**Nym proudly presents NymVPN alpha** - a client that uses [Nym Mixnet](https://nymtech.net) to anonymise all of a user's internet traffic through either a 5-hop mixnet (for a full network privacy) or the faster 2-hop decentralised VPN (with some extra features).
**You are invited to take part in the alpha testing** of this new application. The following pages provide a how-to guide, explaining steps to install and run NymVPN [CLI](cli.md) and [GUI](gui.md) on the Sandbox testnet environment.
**You are invited to take part in the alpha testing** of this new application. The following pages provide a how-to guide, explaining steps to install and run NymVPN [CLI](cli.md) and [GUI](gui.md).
**Here is how**
1. Go to the NymVPN [testers form]({{nym_vpn_form_url}})
2. Please consent to the GDPR so we can use the results
2. Fill and submit the [form!]({{nym_vpn_form_url}})
3. To test the GUI, [go here](gui.md)
4. To test the CLI, [go here](cli.md)
5. Fill and submit the [form!]({{nym_vpn_form_url}})
6. Join the [NymVPN matrix channel](https://matrix.to/#/#NymVPN:nymtech.chat) if you have any questions, comments or blockers
5. Join the [NymVPN matrix channel](https://matrix.to/#/#NymVPN:nymtech.chat) if you have any questions, comments or blockers
***NymVPN alpha testing will last from 15th of January - 15th of February.***
@@ -2,6 +2,14 @@
Below are listed some points which may need to be addressed when testing NymVPN alpha. If you crashed into any errors which are not listed, please contact us at the testing workshop or in the NymVPN [Matrix channel](https://matrix.to/#/#NymVPN:nymtech.chat).
#### NymVPN attempts to connect to sandbox testnet
If you testing the latest versions and you correctly expect the client to run over the mainnet, but it listens to `https://sandbox-nym-api1.nymtech.net`, it's probably because of your previous configuration.
Check your `config.toml` either in the directory from which you run your client or in `~/.config/nym-vpn/` and remove `sandbox.env` from the config file and folder.
If the problem persists (probably due to some locally cache) download [`mainnet.env`](https://github.com/nymtech/nym/blob/master/envs/mainnet.env) and save it to the same directory.
#### Running GUI failed due to `TOML parse error`
If you see this error when running NymVPN alpha desktop, it's because the older versions needed entry location in `config.toml` configuration file. From `v0.0.3` the entry location is selected directly by the user in the application. This error is due to an old `app-data.toml` config in your computer.
+3 -2
View File
@@ -1,3 +1,4 @@
#
# Summary
[Introduction](introduction.md)
@@ -14,7 +15,7 @@
<!-- - [Version Compatibility Table](binaries/version-compatiblity.md) -->
# Nodes
- [Node Types (Previously Setup Guides)](nodes/overview.md)
- [Node Types (Previously Setup Guides)](nodes/overview.md)
- [Mix Node](nodes/mixnode.md)
- [Gateway](nodes/gateway.md)
- [Network Requester](nodes/network-requester.md)
@@ -56,7 +57,7 @@
- [Mixnet Explorer](explorers/mixnet-explorer.md)
# Nyx Blockchain
- [Interacting with Nyx Chain and Smart Contracts](nyx/interacting-with-chain.md)
- [Interacting with Nyx Chain and Smart Contracts](nyx/interacting-with-chain.md)
- [Smart Contracts](nyx/smart-contracts.md)
- [Mixnet Contract](nyx/mixnet-contract.md)
- [Vesting Contract](nyx/vesting-contract.md)
+6
View File
@@ -1,3 +1,4 @@
#
# Summary
- [Introduction](introduction.md)
@@ -28,6 +29,11 @@
- [ExploreNYM scripts](testing/explorenym-scripts.md)
<!-- - [Run in a Docker](testing/docker-monitor.md) -->
- [Troubleshooting](nodes/troubleshooting.md)
<!--
- [Nym Nodes]()
- [Validators]
- [Binary]
-->
# Token Economics
+64 -9
View File
@@ -7,7 +7,7 @@
## What is the Nym API?
The Nym API is a binary that will be operated by the Nyx validator set. This binary can be run in several different modes, and has two main bits of functionality:
* network monitoring (calculating the routing score of Mixnet nodes)
* generation and validation of [zk-Nyms](https://blog.nymtech.net/zk-nyms-are-here-a-major-milestone-towards-a-market-ready-mixnet-a3470c9ab10a), our implementation of the Coconut Selective Disclosure Credential Scheme.
* generation and validation of [zk-nyms](https://blog.nymtech.net/zk-nyms-are-here-a-major-milestone-towards-a-market-ready-mixnet-a3470c9ab10a), our implementation of the Coconut Selective Disclosure Credential Scheme.
This is important for both the proper decentralisation of the network uptime calculation and, more pressingly, enabling the NymVPN to utilise privacy preserving payments.
@@ -77,18 +77,18 @@ pruning-interval = "100"
The example value of `100` for `pruning-interval` can be customised as per your requirement.
### (Coming Soon) Credential Generation
Validators that take part in the DKG ceremony will become part of the 'quorum' generating and verifying zk-Nym credentials. These will initially be used for private proof of payment for NymVPN (see our blogposts [here](https://blog.nymtech.net/nymvpn-an-invitation-for-privacy-experts-and-enthusiasts-63644139d09d) and [here](https://blog.nymtech.net/zk-nyms-are-here-a-major-milestone-towards-a-market-ready-mixnet-a3470c9ab10a) for more on this), and in the future will be expanded into more general usecases such as [offline ecash](https://arxiv.org/abs/2303.08221).
### Credential Generation
Validators that take part in the DKG ceremony will become part of the 'quorum' generating and verifying zk-nym credentials. These will initially be used for private proof of payment for NymVPN (see our blogposts [here](https://blog.nymtech.net/nymvpn-an-invitation-for-privacy-experts-and-enthusiasts-63644139d09d) and [here](https://blog.nymtech.net/zk-nyms-are-here-a-major-milestone-towards-a-market-ready-mixnet-a3470c9ab10a) for more on this), and in the future will be expanded into more general usecases such as [offline ecash](https://arxiv.org/abs/2303.08221).
The DKG ceremony will be used to create a subset of existing validators - referred to as the quorum. As outlined above, they will be the ones taking part in the generation and verification of zk-Nym credentials. The size of the 'minimum viable quorum' is 10 - we are aiming for a larger number than this for the initial quorum in order to have some redundancy in the case of a Validator dropping or going offline.
The DKG ceremony will be used to create a subset of existing validators who run `nym-api` alongside a Nyx full-node. As outlined above, they will be the ones taking part in the generation and verification of zk-nym credentials. The size of the 'minimum viable quorum' is 10 - we are aiming for a larger number than this for the initial quorum in order to have some redundancy in the case of a validator dropping or going offline.
We will be releasing more detailed step-by-step documentation for involved validators nearer to the ceremony itself, but at a high level it will involve:
* the deployment and initialisation of [`group`](https://github.com/nymtech/nym/tree/develop/contracts/multisig/cw4-group) and [`multisig`](https://github.com/nymtech/nym/tree/develop/contracts/multisig) contracts by Nym. Validators that are members of the `group` contract are the only ones that will be able to take part in the ceremony.
* the deployment and initialisation of an instance of the [DKG contract](https://github.com/nymtech/nym/tree/develop/contracts/coconut-dkg) by Nym.
* Validators will update their `nym-api` configs with the address of the deployed contracts. They will also stop running their API instance in caching only mode, instead switching over run with the `--enabled-credentials-mode`.
* From the perspective of operators, this is all they have to do. Under the hood, each `nym-api` instance will then take part in several rounds of key submission, verification, and derivation. This will continue until quorum is acheived. More information on this will be released closer to the time of the ceremony.
* From the perspective of operators, this is all they have to do. Under the hood, each `nym-api` instance will then take part in several rounds of key submission, verification, and derivation. This will continue until quorum is acheived.
**We will be communicating individually with members of the existing Validator set who have expressed interest in joining the quorum concerning the timing and specifics of the ceremony**.
**We will be communicating individually with members of the existing validator set who have expressed interest in joining the quorum concerning the timing and specifics of the ceremony**.
## Current version
```
@@ -117,7 +117,7 @@ You can also check the various arguments required for individual commands with:
./nym-api <COMMAND> --help
```
### Initialising your Nym API Instance
### Initialising your Nym API Instance in caching mode
Initialise your API instance with:
```
@@ -126,17 +126,70 @@ Initialise your API instance with:
You can optionally pass a local identifier for this instance with the `--id` flag. Otherwise the ID of your instance defaults to `default`.
### Enabling credential signing on your Nym API instance
To engage in the Distributed Key Generation (DKG) ceremony, it's essential to transition your `nym-api` instance from its default caching mode to the active credential signing mode. This section guides you through the process of enabling credential signing
#### Generate a new wallet
Begin by generating a new wallet address specifically for your instance to use in credential signing mode. Utilize the `nyxd` command-line tool with the following command:
```
nyxd keys add signer
```
~~~admonish warning title="Backup your keys!"
It's critical to securely back up the mnemonic phrase generated during this process. This mnemonic is your key to recovering the wallet in the future, so store it in a secure, offline location.
~~~
#### Fund the address
Next, deposit NYM tokens into the newly created wallet address to ensure it can cover transaction fees incurred during the credential signing process. `nym-api` will not operate if the wallet's balance falls below 10 NYM tokens, displaying an error message upon startup.
We recommand beginning with an initial deposit of 100 NYM tokens and monitoring the balance regularly, topping it up as necessary to maintain operational readiness.
#### Update API configuration
With your new wallet ready and funded, proceed to update your `nym-api` configuration to enable credential signing:
Update your `config.toml` located in `<HOME_DIR>/.nym/nym-api/foo/config/config.toml` as below:
Enable the coconut signer:
```
[coconut_signer]
# Specifies whether coconut signing protocol is enabled in this process.
enabled = true # This was previously false
```
Set your announce address if it is empty. This is the URL you previously configured for your `nym-api` instance
```
# This is the address you previously configured for the nym-api
# Not to be confused with the Cosmos REST API URL
announce_address = 'https://nym-api.your.tld/'
```
Finally, input the mnemonic phrase generated during the wallet creation step into the mnemonic field
```
mnemonic = '<YOUR_MNEMONIC_HERE>'
```
After completing these steps, your `nym-api` instance is configured to participate in credential signing and the DKG ceremony.
### Running your Nym API Instance
The API binary currently defaults to running in caching mode. You can run your API with:
```
./nym-api run
./nym-api run --id <YOUR_ID>
```
By default the API will be trying to query your full node running locally on `localhost:26657`. If your node is hosted elsewhere, you can specify the RPC location by using the `--nyxd-validator ` flag on `run`:
```
./nym-api run --nyxd-validator https://rpc-nym.yourcorp.tld:443
./nym-api run --id <YOUR_ID> --nyxd-validator https://rpc-nym.yourcorp.tld:443
```
> You can also change the value of `local_validator` in the config file found by default in `$HOME/.nym/nym-api/<ID>/config/config.toml`.
@@ -226,6 +279,8 @@ Starting nym api...
## Automation
You will most likely want to automate your validator restarting if your server reboots. Checkout the [maintenance page](./maintenance.md) for an example `service` file.
You can also use `nymvisor` to automatically update the `nym-api` node. The steps to install Nymvisor can be found [here](nymvisor-upgrade.md).
## Exposing web endpoint using HTTPS
It is recommended to expose the webserver over HTTPS by using a webserver like Nginx. An example configuration for configuring Nginx is listed [on the maintenance page](maintenance.md#nym-api-configuration). If you're using a custom solution, ensure to allow requests from anywhere by setting a permissive CORS policy.
+3
View File
@@ -0,0 +1,3 @@
# cmdrun scripts
A place for all scripts used for `mdbook-cmdrun` to auto-fill the pages with up todate info, syntax and command outputs.
+182
View File
@@ -0,0 +1,182 @@
import argparse
import os
import requests
import json
import sys
import pandas as pd
from collections import namedtuple
############################################
############## GENERAL FNs #################
############################################
def get_url(args):
config_file = "./api_targets_config.json"
with open(config_file, "r") as f:
config = json.load(f)
env = args.api
endpoint = args.endpoint
if env == "github":
url = f"{config[env]}/{endpoint}"
else:
url = f"{config[env]}/api/v1/{endpoint}"
return url
def subparser_read(args):
url = get_url(args)
r = requests.get(url)
response = r.json()
return response
############################################
########### NYX RELATED FNs ################
############################################
def convert_u_nym(unym):
unym = int(unym)
nym = unym / 1000000
nym = int(nym)
return nym
def thousand_separator(n):
n = f'{n:_}'
return n
def remove_underscore(arg):
string = arg.replace("_", " ")
string = string.title()
return string
def display_supply_table(response, args):
df = pd.DataFrame(response)
df = df.T
del df['denom']
# df.set_axis(['**Item**', '**Amount in NYM**'], axis=1, inplace=True)
df = df.rename_axis('index1').reset_index()
df = df.rename(columns={'index1': '**Item**', 'amount': '**Amount in NYM**'})
df['**Item**'] = df['**Item**'].apply(remove_underscore)
df['**Amount in NYM**'] = df['**Amount in NYM**'].apply(convert_u_nym)
df['**Amount in NYM**'] = df['**Amount in NYM**'].apply(thousand_separator)
table = df.to_markdown(index=False)
print(table)
def read_supply(args):
response = subparser_read(args)
if args.endpoint == "circulating-supply":
if args.format:
display_supply_table(response, args)
else:
print(response)
elif args.endpoint == "foo":
# placeholder for other endpoint args
pass
else:
# placeholder for other endpoint args
pass
###########################################
############ GH RELATED FNs ###############
###########################################
def get_nym_vpn_version(args):
response = subparser_read(args)
if args.client == "desktop":
version = current_desktop_version(args, response)
elif args.client == "cli":
version = current_cli_version(args, response)
else:
print("Incorrect argument for -c, --client")
sys.exit(-1)
def current_cli_version(args, response):
df = pd.DataFrame(response)
print(df)
# NEEDS THIS IN PYTHON:
# current_cli_version=$(curl -s $release_url | jq -r '.[].tag_name' | grep '^nym-vpn-cli-v' | sort -Vr | head -n 1 | awk -F'-v' '{print $NF}')
def current_desktop_version(args, response):
# NEEDS THIS IN PYTHON:
df = pd.DataFrame(response)
print(df)
# version=$(curl -s $release_url | jq -r '.[].tag_name' | grep '^nym-vpn-desktop-v' | sort -Vr | head -n 1 | awk -F'-v' '{print $NF}')
###########################################
############### MAIN PARSER ###############
###########################################
def parser_main():
parser = argparse.ArgumentParser(
prog="Nym API scraper",
description='''Get any live data from Nyx validator''',
epilog=''
)
subparsers = parser.add_subparsers(help="")
parser_supply = subparsers.add_parser('supply',
help='reads API on supply',
aliases=['s','S']
)
parser_supply.add_argument(
"-a","--api",
type=str,
default="mainnet",
help="choose: mainnet, perf, sandbox"
)
parser_supply.add_argument(
"-e","--endpoint",
type=str,
help="choose from: https://validator.nymtech.net/api/swagger/index.html"
)
parser_supply.add_argument(
"-f","--format",
action="store_true",
help="format the output for documentation purpose (.md) - default: False (raw output)",
)
parser_supply.set_defaults(func=read_supply)
parser_nym_vpn = subparsers.add_parser('nym_vpn',
help='reads NymVPN latest version',
aliases=['n','N']
)
parser_nym_vpn.add_argument(
"-c","--client",
type=str,
default="desktop",
help="choose: desktop, cli - default: desktop"
)
parser_nym_vpn.add_argument(
"-a","--api",
type=str,
default="github",
help="choose: mainnet, perf, sandbox"
)
parser_nym_vpn.add_argument(
"-e","--endpoint",
type=str,
help="add the url suffix",
default="repos/nymtech/nym-vpn-client/releases"
)
parser_nym_vpn.set_defaults(func=get_nym_vpn_version)
args = parser.parse_args()
try:
args.func(args)
except AttributeError as e:
print("Error on argparser")
sys.exit(-1)
if __name__ == "__main__":
parser_main()
@@ -0,0 +1,6 @@
{
"sandbox": "https://sandbox-nym-api1.nymtech.net",
"perf": "https://api.performance.nymte.ch",
"mainnet": "https://validator.nymtech.net",
"github": "https://api.github.com"
}
+4 -4
View File
@@ -13,10 +13,10 @@ DENOMS_EXPONENT=6
REWARDING_VALIDATOR_ADDRESS=n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa
MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav
VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n13902g92xfefeyzuyed49snlm5fxv5ms6mdq5kvrut27hasdw5a9q9vyw6c
GROUP_CONTRACT_ADDRESS=n18nczmqw6adwxg2wnlef3hf0etf8anccafp2pjpul5rrtmv96umyq5mv7t5
MULTISIG_CONTRACT_ADDRESS=n1q3zzxl78rlmxv3vn0uf4vkyz285lk8q2xzne299yt9x6mpfgk90qukuzmv
COCONUT_DKG_CONTRACT_ADDRESS=n1jsz20ggp5a6v76j060erkzvxmeus8htlpl77yxp878f0gf95cyaq6p2pee
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ljlwey4xdj0zs7zueepc48nkr033fca6fjgvurfvttqegm8dvsrswsul70
GROUP_CONTRACT_ADDRESS=n10v3rjnq4cjyccfykyams68ztce337gksuu6f0lvtl4meuwvkewaqru4uav
MULTISIG_CONTRACT_ADDRESS=n1cemnu8as0ls45v3caunpesl8jlsfw2ff9rlwnltlecp7zrxct4dsqc2y42
COCONUT_DKG_CONTRACT_ADDRESS=n1zx96qgd88vqlzcxkpwzks7kqs5ctrx36xtzfc58p7q6c4ng9anlqzc4nh8
NAME_SERVICE_CONTRACT_ADDRESS=n12ne7qtmdwd0j03t9t5es8md66wq4e5xg9neladrsag8fx3y89rcs36asfp
SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS=n1ps5yutd7sufwg058qd7ac7ldnlazsvmhzqwucsfxmm445d70u8asqxpur4
EPHEMERA_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
+1
View File
@@ -38,6 +38,7 @@ sqlx = { workspace = true, features = [
"sqlite",
"macros",
"migrate",
"time"
] }
subtle-encoding = { version = "0.5", features = ["bech32-preview"] }
thiserror = { workspace = true }
@@ -4,7 +4,7 @@
use nym_crypto::asymmetric::identity;
use thiserror::Error;
#[derive(Debug, Error)]
#[derive(Debug, Clone, Error)]
pub enum HandshakeError {
#[error(
"received key material of invalid length - {0}. Expected: {}",
+25
View File
@@ -190,6 +190,22 @@ impl ClientControlRequest {
}
}
pub fn name(&self) -> String {
match self {
ClientControlRequest::Authenticate { .. } => "Authenticate".to_string(),
ClientControlRequest::RegisterHandshakeInitRequest { .. } => {
"RegisterHandshakeInitRequest".to_string()
}
ClientControlRequest::BandwidthCredential { .. } => "BandwidthCredential".to_string(),
ClientControlRequest::BandwidthCredentialV2 { .. } => {
"BandwidthCredentialV2".to_string()
}
ClientControlRequest::ClaimFreeTestnetBandwidth => {
"ClaimFreeTestnetBandwidth".to_string()
}
}
}
pub fn new_enc_coconut_bandwidth_credential_v1(
credential: &OldV1Credential,
shared_key: &SharedKeys,
@@ -291,6 +307,15 @@ pub enum ServerResponse {
}
impl ServerResponse {
pub fn name(&self) -> String {
match self {
ServerResponse::Authenticate { .. } => "Authenticate".to_string(),
ServerResponse::Register { .. } => "Register".to_string(),
ServerResponse::Bandwidth { .. } => "Bandwidth".to_string(),
ServerResponse::Send { .. } => "Send".to_string(),
ServerResponse::Error { .. } => "Error".to_string(),
}
}
pub fn new_error<S: Into<String>>(msg: S) -> Self {
ServerResponse::Error {
message: msg.into(),
@@ -0,0 +1,7 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
ALTER TABLE available_bandwidth
ADD COLUMN freepass_expiration TIMESTAMP WITHOUT TIME ZONE;
+13
View File
@@ -42,6 +42,9 @@ const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000;
const DEFAULT_STORED_MESSAGE_FILENAME_LENGTH: u16 = 16;
const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100;
const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_millis(5);
const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 512 * 1024; // 512kB
/// Derive default path to gateway's config directory.
/// It should get resolved to `$HOME/.nym/gateways/<id>/config`
pub fn default_config_directory<P: AsRef<Path>>(id: P) -> PathBuf {
@@ -516,6 +519,13 @@ pub struct Debug {
/// Number of messages from offline client that can be pulled at once from the storage.
pub message_retrieval_limit: i64,
/// Defines maximum delay between client bandwidth information being flushed to the persistent storage.
#[serde(with = "humantime_serde")]
pub client_bandwidth_max_flushing_rate: Duration,
/// Defines a maximum change in client bandwidth before it gets flushed to the persistent storage.
pub client_bandwidth_max_delta_flushing_amount: i64,
/// Specifies whether the mixnode should be using the legacy framing for the sphinx packets.
// it's set to true by default. The reason for that decision is to preserve compatibility with the
// existing nodes whilst everyone else is upgrading and getting the code for handling the new field.
@@ -533,6 +543,9 @@ impl Default for Debug {
maximum_connection_buffer_size: DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE,
stored_messages_filename_length: DEFAULT_STORED_MESSAGE_FILENAME_LENGTH,
message_retrieval_limit: DEFAULT_MESSAGE_RETRIEVAL_LIMIT,
client_bandwidth_max_flushing_rate: DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE,
client_bandwidth_max_delta_flushing_amount:
DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT,
use_legacy_framed_packet_version: false,
}
}
+1
View File
@@ -165,6 +165,7 @@ impl From<ConfigV1_1_31> for Config {
stored_messages_filename_length: value.debug.stored_messages_filename_length,
message_retrieval_limit: value.debug.message_retrieval_limit,
use_legacy_framed_packet_version: value.debug.use_legacy_framed_packet_version,
..Default::default()
},
}
}
+1
View File
@@ -33,6 +33,7 @@ fn load_gateway_details(
};
Ok(api_requests::v1::gateway::models::Gateway {
enforces_zk_nyms: config.gateway.only_coconut_credentials,
client_interfaces: api_requests::v1::gateway::models::ClientInterfaces {
wireguard,
mixnet_websockets: Some(api_requests::v1::gateway::models::WebSockets {
+23 -17
View File
@@ -42,18 +42,35 @@ pub struct Bandwidth {
}
impl Bandwidth {
pub const fn new(value: u64) -> Bandwidth {
pub const fn new_unchecked(value: u64) -> Bandwidth {
Bandwidth { value }
}
pub fn try_from_raw_value(value: &str, typ: CredentialType) -> Result<Self, BandwidthError> {
let bandwidth_value =
pub fn new(bandwidth_value: u64) -> Result<Bandwidth, BandwidthError> {
if bandwidth_value > i64::MAX as u64 {
// note that this would have represented more than 1 exabyte,
// which is like 125,000 worth of hard drives, so I don't think we have
// to worry about it for now...
warn!("Somehow we received bandwidth value higher than 9223372036854775807. We don't really want to deal with this now");
return Err(BandwidthError::UnsupportedBandwidthValue(bandwidth_value));
}
Ok(Bandwidth {
value: bandwidth_value,
})
}
pub(crate) fn parse_raw_bandwidth(
value: &str,
typ: CredentialType,
) -> Result<(u64, Option<OffsetDateTime>), BandwidthError> {
let (bandwidth_value, freepass_expiration) =
match typ {
CredentialType::Voucher => {
let token_value: u64 = value
.parse()
.map_err(|source| BandwidthError::VoucherValueParsingFailure { source })?;
token_value * nym_network_defaults::BYTES_PER_UTOKEN
(token_value * nym_network_defaults::BYTES_PER_UTOKEN, None)
}
CredentialType::FreePass => {
let expiry_timestamp: i64 = value
@@ -70,21 +87,10 @@ impl Bandwidth {
if expiry_date < now {
return Err(BandwidthError::ExpiredFreePass { expiry_date });
}
nym_network_defaults::BYTES_PER_FREEPASS
(nym_network_defaults::BYTES_PER_FREEPASS, Some(expiry_date))
}
};
if bandwidth_value > i64::MAX as u64 {
// note that this would have represented more than 1 exabyte,
// which is like 125,000 worth of hard drives, so I don't think we have
// to worry about it for now...
warn!("Somehow we received bandwidth value higher than 9223372036854775807. We don't really want to deal with this now");
return Err(BandwidthError::UnsupportedBandwidthValue(bandwidth_value));
}
Ok(Bandwidth {
value: bandwidth_value,
})
Ok((bandwidth_value, freepass_expiration))
}
pub fn value(&self) -> u64 {
+2 -1
View File
@@ -8,4 +8,5 @@ mod bandwidth;
pub(crate) mod embedded_clients;
pub(crate) mod websocket;
pub(crate) const FREE_TESTNET_BANDWIDTH_VALUE: Bandwidth = Bandwidth::new(64 * 1024 * 1024 * 1024); // 64GB
pub(crate) const FREE_TESTNET_BANDWIDTH_VALUE: Bandwidth =
Bandwidth::new_unchecked(64 * 1024 * 1024 * 1024); // 64GB
@@ -0,0 +1,16 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier;
use crate::node::client_handling::websocket::connection_handler::BandwidthFlushingBehaviourConfig;
use nym_crypto::asymmetric::identity;
use std::sync::Arc;
// I can see this being possible expanded with say storage or client store
#[derive(Clone)]
pub(crate) struct CommonHandlerState {
pub(crate) coconut_verifier: Arc<CoconutVerifier>,
pub(crate) local_identity: Arc<identity::KeyPair>,
pub(crate) only_coconut_credentials: bool,
pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig,
}
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::client_handling::bandwidth::BandwidthError;
use crate::node::client_handling::websocket::connection_handler::ClientBandwidth;
use crate::node::{
client_handling::{
bandwidth::Bandwidth,
@@ -34,6 +35,7 @@ use nym_validator_client::coconut::CoconutApiError;
use rand::{CryptoRng, Rng};
use std::{process, time::Duration};
use thiserror::Error;
use time::OffsetDateTime;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError};
@@ -42,6 +44,11 @@ pub enum RequestHandlingError {
#[error("Internal gateway storage error")]
StorageError(#[from] StorageError),
#[error(
"the database entry for bandwidth of the registered client {client_address} is missing!"
)]
MissingClientBandwidthEntry { client_address: String },
#[error("Provided bandwidth IV is malformed - {0}")]
MalformedIV(#[from] IVConversionError),
@@ -51,8 +58,8 @@ pub enum RequestHandlingError {
#[error("Provided binary request was malformed - {0}")]
InvalidTextRequest(<ClientControlRequest as TryFrom<String>>::Error),
#[error("The received request is not valid in the current context")]
IllegalRequest,
#[error("The received request is not valid in the current context: {additional_context}")]
IllegalRequest { additional_context: String },
#[error("Provided bandwidth credential did not verify correctly on {0}")]
InvalidBandwidthCredential(String),
@@ -90,9 +97,18 @@ pub enum RequestHandlingError {
#[error("the provided credential did not contain a valid type attribute")]
InvalidTypeAttribute,
#[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")]
OutOfBandwidth { required: i64, available: i64 },
#[error("the provided credential did not have a bandwidth attribute")]
MissingBandwidthAttribute,
#[error("attempted to claim a bandwidth voucher for an account using a free pass (it expires on {expiration})")]
BandwidthVoucherForFreePassAccount { expiration: OffsetDateTime },
#[error("attempted to claim another free pass for the account while another free pass is still active (it expires on {expiration})")]
PreexistingFreePass { expiration: OffsetDateTime },
#[error("the DKG contract is unavailable")]
UnavailableDkgContract,
}
@@ -121,6 +137,7 @@ impl IntoWSMessage for Result<ServerResponse, RequestHandlingError> {
pub(crate) struct AuthenticatedHandler<R, S, St> {
inner: FreshHandler<R, S, St>,
client: ClientDetails,
client_bandwidth: ClientBandwidth,
mix_receiver: MixMessageReceiver,
// Occasionally the handler is requested to ping the connected client for confirm that it's
// active, such as when a duplicate connection is detected. This hashmap stores the oneshot
@@ -153,19 +170,32 @@ where
/// * `fresh`: fresh, unauthenticated, connection handler.
/// * `client`: details (i.e. address and shared keys) of the registered client
/// * `mix_receiver`: channel used for receiving messages from the mixnet destined for this client.
pub(crate) fn upgrade(
pub(crate) async fn upgrade(
fresh: FreshHandler<R, S, St>,
client: ClientDetails,
mix_receiver: MixMessageReceiver,
is_active_request_receiver: IsActiveRequestReceiver,
) -> Self {
AuthenticatedHandler {
) -> Result<Self, RequestHandlingError> {
// note: the `upgrade` function can only be called after registering or authenticating the client,
// meaning the appropriate database rows must have been created
// so in theory we could just unwrap the value here, but since we're returning a Result anyway,
// we might as well return a failure response instead
let bandwidth = fresh
.storage
.get_available_bandwidth(client.address)
.await?
.ok_or(RequestHandlingError::MissingClientBandwidthEntry {
client_address: client.address.as_base58_string(),
})?;
Ok(AuthenticatedHandler {
inner: fresh,
client,
client_bandwidth: ClientBandwidth::new(bandwidth.into()),
mix_receiver,
is_active_request_receiver,
is_active_ping_pending_reply: None,
}
})
}
/// Explicitly removes handle from the global store.
@@ -175,15 +205,10 @@ where
.disconnect(self.client.address)
}
/// Checks the amount of bandwidth available for the connected client.
async fn get_available_bandwidth(&self) -> Result<i64, RequestHandlingError> {
let bandwidth = self
.inner
.storage
.get_available_bandwidth(self.client.address)
.await?
.unwrap_or_default();
Ok(bandwidth)
async fn expire_freepass(&mut self) -> Result<(), RequestHandlingError> {
self.client_bandwidth.bandwidth = Default::default();
self.client_bandwidth.update_flush_data();
Ok(self.inner.expire_freepass(self.client.address).await?)
}
/// Increases the amount of available bandwidth of the connected client by the specified value.
@@ -191,11 +216,27 @@ where
/// # Arguments
///
/// * `amount`: amount to increase the available bandwidth by.
async fn increase_bandwidth(&self, bandwidth: Bandwidth) -> Result<(), RequestHandlingError> {
async fn increase_bandwidth(
&mut self,
bandwidth: Bandwidth,
) -> Result<(), RequestHandlingError> {
self.client_bandwidth.bandwidth.bytes += bandwidth.value() as i64;
// any increases to bandwidth should get flushed immediately
// (we don't want to accidentally miss somebody claiming a gigabyte voucher)
self.flush_bandwidth().await
}
async fn set_freepass_expiration(
&mut self,
expiration: OffsetDateTime,
) -> Result<(), RequestHandlingError> {
self.client_bandwidth.bandwidth.freepass_expiration = Some(expiration);
self.inner
.storage
.increase_bandwidth(self.client.address, bandwidth.value() as i64)
.set_freepass_expiration(self.client.address, expiration)
.await?;
self.client_bandwidth.update_flush_data();
Ok(())
}
@@ -204,11 +245,18 @@ where
/// # Arguments
///
/// * `amount`: amount to decrease the available bandwidth by.
async fn consume_bandwidth(&self, amount: i64) -> Result<(), RequestHandlingError> {
self.inner
.storage
.consume_bandwidth(self.client.address, amount)
.await?;
async fn consume_bandwidth(&mut self, amount: i64) -> Result<(), RequestHandlingError> {
self.client_bandwidth.bandwidth.bytes -= amount;
// since we're going to be operating on a fair use policy anyway, even if we crash and let extra few packets
// through, that's completely fine
if self
.client_bandwidth
.should_flush(self.inner.shared_state.bandwidth_cfg)
{
self.flush_bandwidth().await?;
}
Ok(())
}
@@ -232,6 +280,22 @@ where
let serial_number = credential.data.blinded_serial_number();
trace!("processing credential {}", serial_number.to_bs58());
// if we already have had received a free pass (that's not expired, don't accept any additional bandwidth)
if self.client_bandwidth.bandwidth.freepass_expired() {
// the free pass we used before has expired -> reset our state and handle the request as normal
self.expire_freepass().await?;
} else if let Some(expiration) = self.client_bandwidth.bandwidth.freepass_expiration {
// the free pass is still valid -> return error
return match credential.data.typ {
CredentialType::Voucher => {
Err(RequestHandlingError::BandwidthVoucherForFreePassAccount { expiration })
}
CredentialType::FreePass => {
Err(RequestHandlingError::PreexistingFreePass { expiration })
}
};
}
let already_spent = self
.inner
.storage
@@ -247,12 +311,6 @@ where
credential.data.epoch_id
);
let aggregated_verification_key = self
.inner
.coconut_verifier
.verification_key(credential.data.epoch_id)
.await?;
if !credential.data.validate_type_attribute() {
trace!("mismatch in the type attribute");
return Err(RequestHandlingError::InvalidTypeAttribute);
@@ -264,41 +322,52 @@ where
};
// this will extract token amounts out of bandwidth vouchers and validate expiry of free passes
let bandwidth = Bandwidth::try_from_raw_value(bandwidth_attribute, credential.data.typ)?;
let (raw_bandwidth, freepass_expiration) =
Bandwidth::parse_raw_bandwidth(bandwidth_attribute, credential.data.typ)?;
let bandwidth = Bandwidth::new(raw_bandwidth)?;
trace!("embedded bandwidth: {bandwidth:?}");
// locally verify the credential
let params = bandwidth_credential_params();
if !credential.data.verify(params, &aggregated_verification_key) {
trace!("the credential did not verify correctly");
return Err(RequestHandlingError::InvalidBandwidthCredential(
String::from("local credential verification has failed"),
));
{
let aggregated_verification_key = self
.inner
.shared_state
.coconut_verifier
.verification_key(credential.data.epoch_id)
.await?;
let params = bandwidth_credential_params();
if !credential.data.verify(params, &aggregated_verification_key) {
trace!("the credential did not verify correctly");
return Err(RequestHandlingError::InvalidBandwidthCredential(
String::from("local credential verification has failed"),
));
}
}
let was_freepass = match credential.data.typ {
match credential.data.typ {
CredentialType::Voucher => {
trace!("the credential is a bandwidth voucher. attempting to release the funds");
let api_clients = self
.inner
.shared_state
.coconut_verifier
.api_clients(credential.data.epoch_id)
.await?;
self.inner
.shared_state
.coconut_verifier
.release_bandwidth_voucher_funds(&api_clients, credential)
.await?;
false
}
CredentialType::FreePass => {
// no need to do anything special here, we already extracted the bandwidth amount and checked expiry
info!("received a free pass credential");
true
}
};
}
// technically this is not atomic, i.e. checking for the spending and then marking as spent,
// but because we have the `UNIQUE` constraint on the database table
@@ -311,12 +380,21 @@ where
trace!("storing serial number information");
self.inner
.storage
.insert_spent_credential(serial_number, was_freepass, self.client.address)
.insert_spent_credential(
serial_number,
freepass_expiration.is_some(),
self.client.address,
)
.await?;
trace!("increasing client bandwidth");
self.increase_bandwidth(bandwidth).await?;
let available_total = self.get_available_bandwidth().await?;
// set free pass expiration
if let Some(expiration) = freepass_expiration {
self.set_freepass_expiration(expiration).await?;
}
let available_total = self.client_bandwidth.bandwidth.bytes;
Ok(ServerResponse::Bandwidth { available_total })
}
@@ -367,17 +445,47 @@ where
) -> Result<ServerResponse, RequestHandlingError> {
debug!("handling testnet bandwidth request");
if self.inner.only_coconut_credentials {
if self.inner.shared_state.only_coconut_credentials {
return Err(RequestHandlingError::OnlyCoconutCredentials);
}
self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE)
.await?;
let available_total = self.get_available_bandwidth().await?;
let available_total = self.client_bandwidth.bandwidth.bytes;
Ok(ServerResponse::Bandwidth { available_total })
}
async fn flush_bandwidth(&mut self) -> Result<(), RequestHandlingError> {
trace!("flushing client bandwidth to the underlying storage");
self.inner
.storage
.set_bandwidth(self.client.address, self.client_bandwidth.bandwidth.bytes)
.await?;
self.client_bandwidth.update_flush_data();
Ok(())
}
async fn try_use_bandwidth(
&mut self,
required_bandwidth: i64,
) -> Result<i64, RequestHandlingError> {
if self.client_bandwidth.bandwidth.freepass_expired() {
self.expire_freepass().await?;
}
let available_bandwidth = self.client_bandwidth.bandwidth.bytes;
if available_bandwidth < required_bandwidth {
return Err(RequestHandlingError::OutOfBandwidth {
required: required_bandwidth,
available: available_bandwidth,
});
}
self.consume_bandwidth(required_bandwidth).await?;
Ok(self.client_bandwidth.bandwidth.bytes)
}
/// Tries to handle request to forward sphinx packet into the network. The request can only succeed
/// if the client has enough available bandwidth.
///
@@ -387,24 +495,16 @@ where
///
/// * `mix_packet`: packet received from the client that should get forwarded into the network.
async fn handle_forward_sphinx(
&self,
&mut self,
mix_packet: MixPacket,
) -> Result<ServerResponse, RequestHandlingError> {
let consumed_bandwidth = mix_packet.packet().len() as i64;
let required_bandwidth = mix_packet.packet().len() as i64;
let available_bandwidth = self.get_available_bandwidth().await?;
if available_bandwidth < consumed_bandwidth {
return Ok(ServerResponse::new_error(
"Insufficient bandwidth available",
));
}
self.consume_bandwidth(consumed_bandwidth).await?;
let remaining_bandwidth = self.try_use_bandwidth(required_bandwidth).await?;
self.forward_packet(mix_packet);
Ok(ServerResponse::Send {
remaining_bandwidth: available_bandwidth - consumed_bandwidth,
remaining_bandwidth,
})
}
@@ -413,7 +513,7 @@ where
/// # Arguments
///
/// * `bin_msg`: raw message to handle.
async fn handle_binary(&self, bin_msg: Vec<u8>) -> Message {
async fn handle_binary(&mut self, bin_msg: Vec<u8>) -> Message {
trace!("binary request");
// this function decrypts the request and checks the MAC
match BinaryRequest::try_from_encrypted_tagged_bytes(bin_msg, &self.client.shared_keys) {
@@ -455,7 +555,13 @@ where
.handle_claim_testnet_bandwidth()
.await
.into_ws_message(),
_ => RequestHandlingError::IllegalRequest.into_error_message(),
other => RequestHandlingError::IllegalRequest {
additional_context: format!(
"received illegal message of type {} in an authenticated client",
other.name()
),
}
.into_error_message(),
},
}
}
@@ -1,6 +1,7 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::RequestHandlingError;
use futures::{
channel::{mpsc, oneshot},
SinkExt, StreamExt,
@@ -20,18 +21,19 @@ use nym_gateway_requests::{
use nym_mixnet_client::forwarder::MixForwardingSender;
use nym_sphinx::DestinationAddressBytes;
use rand::{CryptoRng, Rng};
use std::{sync::Arc, time::Duration};
use std::time::Duration;
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError};
use crate::node::client_handling::websocket::common_state::CommonHandlerState;
use crate::node::client_handling::websocket::connection_handler::AvailableBandwidth;
use crate::node::{
client_handling::{
active_clients::ActiveClientsStore,
websocket::{
connection_handler::{
coconut::CoconutVerifier, AuthenticatedHandler, ClientDetails, InitialAuthResult,
SocketStream,
AuthenticatedHandler, ClientDetails, InitialAuthResult, SocketStream,
},
message_receiver::{IsActive, IsActiveRequestSender},
},
@@ -77,24 +79,55 @@ pub(crate) enum InitialAuthenticationError {
#[error("Attempted to negotiate connection with client using incompatible protocol version. Ours is {current} and the client reports {client:?}")]
IncompatibleProtocol { client: Option<u8>, current: u8 },
#[error("failed to send authentication error response: {source}")]
ErrorResponseSendFailure {
#[source]
source: WsError,
},
#[error("failed to send authentication response: {source}")]
ResponseSendFailure {
#[source]
source: WsError,
},
#[error("possibly received a sphinx packet without prior authentication. Request is going to be ignored")]
BinaryRequestWithoutAuthentication,
#[error("received a connection close message")]
CloseMessage,
#[error("the connection has unexpectedly closed")]
ClosedConnection,
#[error("failed to obtain message from websocket stream: {source}")]
FailedToReadMessage {
#[source]
source: WsError,
},
#[error("could not establish client details")]
EmptyClientDetails,
#[error("failed to upgrade the client handler: {source}")]
HandlerUpgradeFailure { source: RequestHandlingError },
}
impl InitialAuthenticationError {
/// Converts this Error into an appropriate websocket Message.
fn into_error_message(self) -> Message {
fn to_error_message(&self) -> Message {
ServerResponse::new_error(self.to_string()).into()
}
}
pub(crate) struct FreshHandler<R, S, St> {
rng: R,
local_identity: Arc<identity::KeyPair>,
pub(crate) only_coconut_credentials: bool,
pub(crate) shared_state: CommonHandlerState,
pub(crate) active_clients_store: ActiveClientsStore,
pub(crate) outbound_mix_sender: MixForwardingSender,
pub(crate) socket_connection: SocketStream<S>,
pub(crate) storage: St,
pub(crate) coconut_verifier: Arc<CoconutVerifier>,
// currently unused (but populated)
pub(crate) negotiated_protocol: Option<u8>,
@@ -113,23 +146,19 @@ where
pub(crate) fn new(
rng: R,
conn: S,
only_coconut_credentials: bool,
outbound_mix_sender: MixForwardingSender,
local_identity: Arc<identity::KeyPair>,
storage: St,
active_clients_store: ActiveClientsStore,
coconut_verifier: Arc<CoconutVerifier>,
shared_state: CommonHandlerState,
) -> Self {
FreshHandler {
rng,
active_clients_store,
only_coconut_credentials,
outbound_mix_sender,
socket_connection: SocketStream::RawTcp(conn),
local_identity,
storage,
coconut_verifier,
negotiated_protocol: None,
shared_state,
}
}
@@ -171,7 +200,7 @@ where
gateway_handshake(
&mut self.rng,
ws_stream,
self.local_identity.as_ref(),
self.shared_state.local_identity.as_ref(),
init_msg,
)
.await
@@ -436,7 +465,7 @@ where
// Ask the other connection to ping if they are still active.
// Use a oneshot channel to return the result to us
let (ping_result_sender, ping_result_receiver) = oneshot::channel();
log::debug!("Asking other connection handler to ping the connected client to see if it is still active");
debug!("Asking other connection handler to ping the connected client to see if it is still active");
if let Err(err) = is_active_request_tx.send(ping_result_sender).await {
warn!("Failed to send ping request to other handler: {err}");
}
@@ -448,31 +477,31 @@ where
IsActive::NotActive => {
// The other handler reported that the client is not active, so we can
// disconnect the other client and continue with this connection.
log::debug!("Other handler reports it is not active");
debug!("Other handler reports it is not active");
self.active_clients_store.disconnect(address);
}
IsActive::Active => {
// The other handled reported a positive reply, so we have to assume it's
// still active and disconnect this connection.
log::info!("Other handler reports it is active");
info!("Other handler reports it is active");
return Err(InitialAuthenticationError::DuplicateConnection);
}
IsActive::BusyPinging => {
// The other handler is already busy pinging the client, so we have to
// assume it's still active and disconnect this connection.
log::debug!("Other handler reports it is already busy pinging the client");
debug!("Other handler reports it is already busy pinging the client");
return Err(InitialAuthenticationError::DuplicateConnection);
}
}
}
Ok(Err(_)) => {
// Other channel failed to reply (the channel sender probably dropped)
log::info!("Other connection failed to reply, disconnecting it in favour of this new connection");
info!("Other connection failed to reply, disconnecting it in favour of this new connection");
self.active_clients_store.disconnect(address);
}
Err(_) => {
// Timeout waiting for reply
log::warn!(
warn!(
"Other connection timed out, disconnecting it in favour of this new connection"
);
self.active_clients_store.disconnect(address);
@@ -509,7 +538,7 @@ where
// Check for duplicate clients
if let Some(client_tx) = self.active_clients_store.get_remote_client(address) {
log::warn!("Detected duplicate connection for client: {address}");
warn!("Detected duplicate connection for client: {address}");
self.handle_duplicate_client(address, client_tx.is_active_request_sender)
.await?;
}
@@ -518,11 +547,17 @@ where
.authenticate_client(address, encrypted_address, iv)
.await?;
let status = shared_keys.is_some();
let bandwidth_remaining = self
.storage
.get_available_bandwidth(address)
.await?
.unwrap_or(0);
let available_bandwidth: AvailableBandwidth =
self.storage.get_available_bandwidth(address).await?.into();
let bandwidth_remaining = if available_bandwidth.freepass_expired() {
self.expire_freepass(address).await?;
0
} else {
available_bandwidth.bytes
};
let client_details =
shared_keys.map(|shared_keys| ClientDetails::new(address, shared_keys));
@@ -536,6 +571,13 @@ where
))
}
pub(crate) async fn expire_freepass(
&self,
client: DestinationAddressBytes,
) -> Result<(), StorageError> {
self.storage.reset_freepass_bandwidth(client).await
}
/// Attempts to finalize registration of the client by storing the derived shared keys in the
/// persistent store as well as creating entry for its bandwidth allocation.
///
@@ -657,7 +699,7 @@ where
// TODO: somehow cleanup this method
pub(crate) async fn perform_initial_authentication(
mut self,
) -> Option<AuthenticatedHandler<R, S, St>>
) -> Result<AuthenticatedHandler<R, S, St>, InitialAuthenticationError>
where
S: AsyncRead + AsyncWrite + Unpin + Send,
{
@@ -666,40 +708,51 @@ where
while let Some(msg) = self.read_websocket_message().await {
let msg = match msg {
Ok(msg) => msg,
Err(err) => {
debug!("failed to obtain message from websocket stream! stopping connection handler: {err}");
break;
Err(source) => {
debug!("failed to obtain message from websocket stream! stopping connection handler: {source}");
return Err(InitialAuthenticationError::FailedToReadMessage { source });
}
};
if msg.is_close() {
break;
return Err(InitialAuthenticationError::CloseMessage);
}
// ONLY handle 'Authenticate' or 'Register' requests, ignore everything else
match msg {
Message::Close(_) => break,
// we have explicitly checked for close message
Message::Close(_) => unreachable!(),
Message::Text(text_msg) => {
let (mix_sender, mix_receiver) = mpsc::unbounded();
match self.handle_initial_authentication_request(text_msg).await {
return match self.handle_initial_authentication_request(text_msg).await {
Err(err) => {
if let Err(err) =
self.send_websocket_message(err.into_error_message()).await
debug!("authentication failure: {err}");
// try to send error to the client
if let Err(source) =
self.send_websocket_message(err.to_error_message()).await
{
debug!("Failed to send authentication error response - {err}");
return None;
debug!("Failed to send authentication error response: {source}");
return Err(InitialAuthenticationError::ErrorResponseSendFailure {
source,
});
}
// return the underlying error
Err(err)
}
Ok(auth_result) => {
if let Err(err) = self
// try to send auth response back to the client
if let Err(source) = self
.send_websocket_message(auth_result.server_response.into())
.await
{
debug!("Failed to send authentication response - {err}");
return None;
debug!("Failed to send authentication response: {source}");
return Err(InitialAuthenticationError::ResponseSendFailure {
source,
});
}
return if let Some(client_details) = auth_result.client_details {
if let Some(client_details) = auth_result.client_details {
// Channel for handlers to ask other handlers if they are still active.
let (is_active_request_sender, is_active_request_receiver) =
mpsc::unbounded();
@@ -708,23 +761,29 @@ where
mix_sender,
is_active_request_sender,
);
Some(AuthenticatedHandler::upgrade(
AuthenticatedHandler::upgrade(
self,
client_details,
mix_receiver,
is_active_request_receiver,
))
)
.await
.map_err(|source| {
InitialAuthenticationError::HandlerUpgradeFailure { source }
})
} else {
None
};
// honestly, it's been so long I don't remember under what conditions its possible (if at all)
// to have empty client details
Err(InitialAuthenticationError::EmptyClientDetails)
}
}
}
};
}
Message::Binary(_) => {
// perhaps logging level should be reduced here, let's leave it for now and see what happens
// if client is working correctly, this should have never happened
debug!("possibly received a sphinx packet without prior authentication. Request is going to be ignored");
if let Err(err) = self
if let Err(source) = self
.send_websocket_message(
ServerResponse::new_error(
"binary request without prior authentication",
@@ -733,15 +792,18 @@ where
)
.await
{
debug!("Failed to send error response during authentication: {err}",)
return Err(InitialAuthenticationError::ErrorResponseSendFailure {
source,
});
}
return None;
return Err(InitialAuthenticationError::BinaryRequestWithoutAuthentication);
}
_ => continue,
};
}
None
Err(InitialAuthenticationError::ClosedConnection)
}
pub(crate) async fn start_handling(self, shutdown: nym_task::TaskClient)
@@ -1,6 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::config::Config;
use crate::node::storage::Storage;
use log::{trace, warn};
use nym_gateway_requests::registration::handshake::SharedKeys;
@@ -8,6 +9,8 @@ use nym_gateway_requests::ServerResponse;
use nym_sphinx::DestinationAddressBytes;
use nym_task::TaskClient;
use rand::{CryptoRng, Rng};
use std::time::Duration;
use time::OffsetDateTime;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_tungstenite::WebSocketStream;
use zeroize::{Zeroize, ZeroizeOnDrop};
@@ -103,12 +106,84 @@ pub(crate) async fn handle_connection<R, S, St>(
trace!("received shutdown signal while performing initial authentication");
return;
}
Some(None) => {
warn!("authentication has failed");
Some(Err(err)) => {
warn!("authentication has failed: {err}");
return;
}
Some(Some(auth_handle)) => auth_handle.listen_for_requests(shutdown).await,
Some(Ok(auth_handle)) => auth_handle.listen_for_requests(shutdown).await,
}
trace!("The handler is done!");
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct BandwidthFlushingBehaviourConfig {
/// Defines maximum delay between client bandwidth information being flushed to the persistent storage.
pub client_bandwidth_max_flushing_rate: Duration,
/// Defines a maximum change in client bandwidth before it gets flushed to the persistent storage.
pub client_bandwidth_max_delta_flushing_amount: i64,
}
impl<'a> From<&'a Config> for BandwidthFlushingBehaviourConfig {
fn from(value: &'a Config) -> Self {
BandwidthFlushingBehaviourConfig {
client_bandwidth_max_flushing_rate: value.debug.client_bandwidth_max_flushing_rate,
client_bandwidth_max_delta_flushing_amount: value
.debug
.client_bandwidth_max_delta_flushing_amount,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct AvailableBandwidth {
pub(crate) bytes: i64,
pub(crate) freepass_expiration: Option<OffsetDateTime>,
}
impl AvailableBandwidth {
pub(crate) fn freepass_expired(&self) -> bool {
if let Some(expiration) = self.freepass_expiration {
if expiration < OffsetDateTime::now_utc() {
return true;
}
}
false
}
}
pub(crate) struct ClientBandwidth {
pub(crate) bandwidth: AvailableBandwidth,
pub(crate) last_flushed: OffsetDateTime,
pub(crate) bytes_at_last_flush: i64,
}
impl ClientBandwidth {
pub(crate) fn new(bandwidth: AvailableBandwidth) -> ClientBandwidth {
ClientBandwidth {
bandwidth,
last_flushed: OffsetDateTime::now_utc(),
bytes_at_last_flush: bandwidth.bytes,
}
}
pub(crate) fn should_flush(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool {
if (self.bytes_at_last_flush - self.bandwidth.bytes).abs()
>= cfg.client_bandwidth_max_delta_flushing_amount
{
return true;
}
if self.last_flushed + cfg.client_bandwidth_max_flushing_rate < OffsetDateTime::now_utc() {
return true;
}
false
}
pub(crate) fn update_flush_data(&mut self) {
self.last_flushed = OffsetDateTime::now_utc();
self.bytes_at_last_flush = self.bandwidth.bytes;
}
}
@@ -2,37 +2,26 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::client_handling::active_clients::ActiveClientsStore;
use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier;
use crate::node::client_handling::websocket::common_state::CommonHandlerState;
use crate::node::client_handling::websocket::connection_handler::FreshHandler;
use crate::node::storage::Storage;
use log::*;
use nym_crypto::asymmetric::identity;
use nym_mixnet_client::forwarder::MixForwardingSender;
use rand::rngs::OsRng;
use std::net::SocketAddr;
use std::process;
use std::sync::Arc;
use tokio::task::JoinHandle;
pub(crate) struct Listener {
address: SocketAddr,
local_identity: Arc<identity::KeyPair>,
only_coconut_credentials: bool,
pub(crate) coconut_verifier: Arc<CoconutVerifier>,
shared_state: CommonHandlerState,
}
impl Listener {
pub(crate) fn new(
address: SocketAddr,
local_identity: Arc<identity::KeyPair>,
only_coconut_credentials: bool,
coconut_verifier: Arc<CoconutVerifier>,
) -> Self {
pub(crate) fn new(address: SocketAddr, shared_state: CommonHandlerState) -> Self {
Listener {
address,
local_identity,
only_coconut_credentials,
coconut_verifier,
shared_state,
}
}
@@ -71,12 +60,10 @@ impl Listener {
let handle = FreshHandler::new(
OsRng,
socket,
self.only_coconut_credentials,
outbound_mix_sender.clone(),
Arc::clone(&self.local_identity),
storage.clone(),
active_clients_store.clone(),
Arc::clone(&self.coconut_verifier),
self.shared_state.clone(),
);
let shutdown = shutdown.clone().named(format!("ClientConnectionHandler_{remote_addr}"));
tokio::spawn(async move { handle.start_handling(shutdown).await });
@@ -3,6 +3,9 @@
pub(crate) use listener::Listener;
pub(crate) mod common_state;
pub(crate) mod connection_handler;
pub(crate) mod listener;
pub(crate) mod message_receiver;
pub(crate) use common_state::CommonHandlerState;
+9 -8
View File
@@ -150,7 +150,7 @@ impl<St> Gateway<St> {
network_requester_opts,
ip_packet_router_opts,
client_registry: Arc::new(DashMap::new()),
run_http_server: false,
run_http_server: true,
task_client: None,
})
}
@@ -171,7 +171,7 @@ impl<St> Gateway<St> {
sphinx_keypair,
storage,
client_registry: Arc::new(DashMap::new()),
run_http_server: false,
run_http_server: true,
task_client: None,
}
}
@@ -254,13 +254,14 @@ impl<St> Gateway<St> {
self.config.gateway.clients_port,
);
websocket::Listener::new(
listening_address,
Arc::clone(&self.identity_keypair),
self.config.gateway.only_coconut_credentials,
let shared_state = websocket::CommonHandlerState {
coconut_verifier,
)
.start(
local_identity: Arc::clone(&self.identity_keypair),
only_coconut_credentials: self.config.gateway.only_coconut_credentials,
bandwidth_cfg: (&self.config).into(),
};
websocket::Listener::new(listening_address, shared_state).start(
forwarding_channel,
self.storage.clone(),
active_clients_store,
+57 -37
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::storage::models::{PersistedBandwidth, SpentCredential};
use time::OffsetDateTime;
#[derive(Clone)]
pub(crate) struct BandwidthManager {
@@ -36,6 +37,53 @@ impl BandwidthManager {
Ok(())
}
/// Set the freepass expiration date of the particular client to the provided date.
///
/// # Arguments
///
/// * `client_address_bs58`: base58-encoded address of the client.
/// * `freepass_expiration`: the expiration date of the associated free pass.
pub(crate) async fn set_freepass_expiration(
&self,
client_address_bs58: &str,
freepass_expiration: OffsetDateTime,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
UPDATE available_bandwidth
SET freepass_expiration = ?
WHERE client_address_bs58 = ?
"#,
freepass_expiration,
client_address_bs58
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Reset all the bandwidth associated with the freepass and reset its expiration date
///
/// # Arguments
///
/// * `client_address_bs58`: base58-encoded address of the client.
pub(crate) async fn reset_freepass_bandwidth(
&self,
client_address_bs58: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
UPDATE available_bandwidth
SET available = 0, freepass_expiration = NULL
WHERE client_address_bs58 = ?
"#,
client_address_bs58
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Tries to retrieve available bandwidth for the particular client.
///
/// # Arguments
@@ -45,22 +93,19 @@ impl BandwidthManager {
&self,
client_address_bs58: &str,
) -> Result<Option<PersistedBandwidth>, sqlx::Error> {
sqlx::query_as!(
PersistedBandwidth,
"SELECT * FROM available_bandwidth WHERE client_address_bs58 = ?",
client_address_bs58
)
.fetch_optional(&self.connection_pool)
.await
sqlx::query_as("SELECT * FROM available_bandwidth WHERE client_address_bs58 = ?")
.bind(client_address_bs58)
.fetch_optional(&self.connection_pool)
.await
}
/// Increases available bandwidth of the particular client by the specified amount.
/// Sets available bandwidth of the particular client to the provided amount;
///
/// # Arguments
///
/// * `client_address_bs58`: base58-encoded address of the client.
/// * `amount`: amount of available bandwidth to be added to the client.
pub(crate) async fn increase_available_bandwidth(
/// * `client_address`: address of the client
/// * `amount`: the updated client bandwidth amount.
pub(crate) async fn set_available_bandwidth(
&self,
client_address_bs58: &str,
amount: i64,
@@ -68,32 +113,7 @@ impl BandwidthManager {
sqlx::query!(
r#"
UPDATE available_bandwidth
SET available = available + ?
WHERE client_address_bs58 = ?
"#,
amount,
client_address_bs58
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
/// Decreases available bandwidth of the particular client by the specified amount.
///
/// # Arguments
///
/// * `client_address_bs58`: base58-encoded address of the client.
/// * `amount`: amount of available bandwidth to be removed from the client.
pub(crate) async fn decrease_available_bandwidth(
&self,
client_address_bs58: &str,
amount: i64,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
UPDATE available_bandwidth
SET available = available - ?
SET available = ?
WHERE client_address_bs58 = ?
"#,
amount,
+70 -44
View File
@@ -4,7 +4,7 @@
use crate::node::storage::bandwidth::BandwidthManager;
use crate::node::storage::error::StorageError;
use crate::node::storage::inboxes::InboxManager;
use crate::node::storage::models::{PersistedSharedKeys, StoredMessage};
use crate::node::storage::models::{PersistedBandwidth, PersistedSharedKeys, StoredMessage};
use crate::node::storage::shared_keys::SharedKeysManager;
use async_trait::async_trait;
use log::{debug, error};
@@ -13,6 +13,7 @@ use nym_gateway_requests::registration::handshake::SharedKeys;
use nym_sphinx::DestinationAddressBytes;
use sqlx::ConnectOptions;
use std::path::Path;
use time::OffsetDateTime;
mod bandwidth;
pub(crate) mod error;
@@ -102,6 +103,28 @@ pub trait Storage: Send + Sync {
client_address: DestinationAddressBytes,
) -> Result<(), StorageError>;
/// Set the freepass expiration date of the particular client to the provided date.
///
/// # Arguments
///
/// * `client_address`: address of the client
/// * `freepass_expiration`: the expiration date of the associated free pass.
async fn set_freepass_expiration(
&self,
client_address: DestinationAddressBytes,
freepass_expiration: OffsetDateTime,
) -> Result<(), StorageError>;
/// Reset all the bandwidth associated with the freepass and reset its expiration date
///
/// # Arguments
///
/// * `client_address`: address of the client
async fn reset_freepass_bandwidth(
&self,
client_address: DestinationAddressBytes,
) -> Result<(), StorageError>;
/// Tries to retrieve available bandwidth for the particular client.
///
/// # Arguments
@@ -110,27 +133,15 @@ pub trait Storage: Send + Sync {
async fn get_available_bandwidth(
&self,
client_address: DestinationAddressBytes,
) -> Result<Option<i64>, StorageError>;
) -> Result<Option<PersistedBandwidth>, StorageError>;
/// Increases available bandwidth of the particular client by the specified amount.
/// Sets available bandwidth of the particular client to the provided amount;
///
/// # Arguments
///
/// * `client_address`: address of the client
/// * `amount`: amount of available bandwidth to be added to the client.
async fn increase_bandwidth(
&self,
client_address: DestinationAddressBytes,
amount: i64,
) -> Result<(), StorageError>;
/// Decreases available bandwidth of the particular client by the specified amount.
///
/// # Arguments
///
/// * `client_address`: address of the client
/// * `amount`: amount of available bandwidth to be removed from the client.
async fn consume_bandwidth(
/// * `amount`: the updated client bandwidth amount.
async fn set_bandwidth(
&self,
client_address: DestinationAddressBytes,
amount: i64,
@@ -295,36 +306,44 @@ impl Storage for PersistentStorage {
Ok(())
}
async fn get_available_bandwidth(
async fn set_freepass_expiration(
&self,
client_address: DestinationAddressBytes,
) -> Result<Option<i64>, StorageError> {
let res = self
.bandwidth_manager
.get_available_bandwidth(&client_address.as_base58_string())
.await
.map(|bandwidth_option| bandwidth_option.map(|bandwidth| bandwidth.available))?;
Ok(res)
}
async fn increase_bandwidth(
&self,
client_address: DestinationAddressBytes,
amount: i64,
freepass_expiration: OffsetDateTime,
) -> Result<(), StorageError> {
self.bandwidth_manager
.increase_available_bandwidth(&client_address.as_base58_string(), amount)
.set_freepass_expiration(&client_address.as_base58_string(), freepass_expiration)
.await?;
Ok(())
}
async fn consume_bandwidth(
async fn reset_freepass_bandwidth(
&self,
client_address: DestinationAddressBytes,
) -> Result<(), StorageError> {
self.bandwidth_manager
.reset_freepass_bandwidth(&client_address.as_base58_string())
.await?;
Ok(())
}
async fn get_available_bandwidth(
&self,
client_address: DestinationAddressBytes,
) -> Result<Option<PersistedBandwidth>, StorageError> {
Ok(self
.bandwidth_manager
.get_available_bandwidth(&client_address.as_base58_string())
.await?)
}
async fn set_bandwidth(
&self,
client_address: DestinationAddressBytes,
amount: i64,
) -> Result<(), StorageError> {
self.bandwidth_manager
.decrease_available_bandwidth(&client_address.as_base58_string(), amount)
.set_available_bandwidth(&client_address.as_base58_string(), amount)
.await?;
Ok(())
}
@@ -422,22 +441,29 @@ impl Storage for InMemStorage {
todo!()
}
async fn get_available_bandwidth(
async fn set_freepass_expiration(
&self,
_client_address: DestinationAddressBytes,
) -> Result<Option<i64>, StorageError> {
todo!()
}
async fn increase_bandwidth(
&self,
_client_address: DestinationAddressBytes,
_amount: i64,
_freepass_expiration: OffsetDateTime,
) -> Result<(), StorageError> {
todo!()
}
async fn consume_bandwidth(
async fn reset_freepass_bandwidth(
&self,
_client_address: DestinationAddressBytes,
) -> Result<(), StorageError> {
todo!()
}
async fn get_available_bandwidth(
&self,
_client_address: DestinationAddressBytes,
) -> Result<Option<PersistedBandwidth>, StorageError> {
todo!()
}
async fn set_bandwidth(
&self,
_client_address: DestinationAddressBytes,
_amount: i64,
+22
View File
@@ -1,7 +1,9 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node::client_handling::websocket::connection_handler::AvailableBandwidth;
use sqlx::FromRow;
use time::OffsetDateTime;
pub struct PersistedSharedKeys {
pub(crate) client_address_bs58: String,
@@ -15,10 +17,30 @@ pub struct StoredMessage {
pub(crate) content: Vec<u8>,
}
#[derive(Debug, Clone, FromRow)]
pub struct PersistedBandwidth {
#[allow(dead_code)]
pub(crate) client_address_bs58: String,
pub(crate) available: i64,
pub(crate) freepass_expiration: Option<OffsetDateTime>,
}
impl From<PersistedBandwidth> for AvailableBandwidth {
fn from(value: PersistedBandwidth) -> Self {
AvailableBandwidth {
bytes: value.available,
freepass_expiration: value.freepass_expiration,
}
}
}
impl From<Option<PersistedBandwidth>> for AvailableBandwidth {
fn from(value: Option<PersistedBandwidth>) -> Self {
match value {
None => AvailableBandwidth::default(),
Some(b) => b.into(),
}
}
}
#[derive(Debug, Clone, FromRow)]
+2 -2
View File
@@ -46,7 +46,7 @@ pub struct MixNode {
impl MixNode {
pub fn new(config: Config) -> Result<Self, MixnodeError> {
Ok(MixNode {
run_http_server: false,
run_http_server: true,
descriptor: Self::load_node_description(&config),
identity_keypair: Arc::new(load_identity_keys(&config)?),
sphinx_keypair: Arc::new(load_sphinx_keys(&config)?),
@@ -64,7 +64,7 @@ impl MixNode {
sphinx_keypair: Arc<encryption::KeyPair>,
) -> Self {
MixNode {
run_http_server: false,
run_http_server: true,
task_client: None,
config,
descriptor,
+88 -2
View File
@@ -9,11 +9,11 @@ use nym_mixnet_contract_common::rewarding::RewardEstimate;
use nym_mixnet_contract_common::{
GatewayBond, IdentityKey, Interval, MixId, MixNode, Percent, RewardedSetNodeStatus,
};
use nym_node_requests::api::v1::gateway::models::WebSockets;
use nym_node_requests::api::v1::node::models::{BinaryBuildInformationOwned, HostInformation};
use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::net::IpAddr;
use std::{fmt, time::Duration};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -356,6 +356,54 @@ pub struct CirculatingSupplyResponse {
pub circulating_supply: Coin,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct HostInformation {
pub ip_address: Vec<IpAddr>,
pub hostname: Option<String>,
pub keys: HostKeys,
}
impl From<nym_node_requests::api::v1::node::models::HostInformation> for HostInformation {
fn from(value: nym_node_requests::api::v1::node::models::HostInformation) -> Self {
HostInformation {
ip_address: value.ip_address,
hostname: value.hostname,
keys: value.keys.into(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct HostKeys {
pub ed25519: String,
pub x25519: String,
}
impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys {
fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self {
HostKeys {
ed25519: value.ed25519_identity,
x25519: value.x25519_sphinx,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct WebSockets {
pub ws_port: u16,
pub wss_port: Option<u16>,
}
impl From<nym_node_requests::api::v1::gateway::models::WebSockets> for WebSockets {
fn from(value: nym_node_requests::api::v1::gateway::models::WebSockets) -> Self {
WebSockets {
ws_port: value.ws_port,
wss_port: value.wss_port,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct NymNodeDescription {
pub host_information: HostInformation,
@@ -402,3 +450,41 @@ pub struct IpPacketRouterDetails {
/// address of the embedded ip packet router
pub address: String,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ApiHealthResponse {
pub status: ApiStatus,
pub uptime: u64,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum ApiStatus {
Up,
}
impl ApiHealthResponse {
pub fn new_healthy(uptime: Duration) -> Self {
ApiHealthResponse {
status: ApiStatus::Up,
uptime: uptime.as_secs(),
}
}
}
impl ApiStatus {
pub fn is_up(&self) -> bool {
matches!(self, ApiStatus::Up)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct SignerInformationResponse {
pub cosmos_address: String,
pub identity: String,
pub announce_address: String,
pub verification_key: Option<String>,
}
+4
View File
@@ -67,6 +67,10 @@ fn validate_freepass_public_attributes(res: &FreePassRequest) -> Result<()> {
return Err(CoconutError::TooLongFreePass { expiry_date });
}
if expiry_date < now {
return Err(CoconutError::FreePassExpiryInThePast { expiry_date });
}
Ok(())
}
+3
View File
@@ -82,6 +82,9 @@ pub enum CoconutError {
)]
TooLongFreePass { expiry_date: OffsetDateTime },
#[error("the provided free pass expiry is set in the past!")]
FreePassExpiryInThePast { expiry_date: OffsetDateTime },
#[error("the received bandwidth voucher did not contain deposit value")]
MissingBandwidthValue,
+8
View File
@@ -1,6 +1,7 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_coconut::VerificationKey;
use nym_coconut_dkg_common::types::EpochId;
use nym_dkg::Scalar;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -63,6 +64,13 @@ impl KeyPair {
}
}
pub async fn verification_key(&self) -> Option<RwLockReadGuard<'_, VerificationKey>> {
RwLockReadGuard::try_map(self.get().await?, |maybe_keypair| {
maybe_keypair.as_ref().map(|k| k.keys.verification_key())
})
.ok()
}
pub async fn read_keys(&self) -> RwLockReadGuard<'_, Option<KeyPairWithEpoch>> {
self.keys.read().await
}
+1
View File
@@ -38,6 +38,7 @@ pub(crate) mod node_describe_cache;
pub(crate) mod node_status_api;
pub(crate) mod nym_contract_cache;
pub(crate) mod nym_nodes;
mod status;
pub(crate) mod support;
struct ShutdownHandles {
+3 -3
View File
@@ -181,11 +181,11 @@ async fn get_gateway_description(
};
let description = NymNodeDescription {
host_information: host_info.data,
host_information: host_info.data.into(),
build_information: build_info,
network_requester,
ip_packet_router,
mixnet_websockets: websockets,
mixnet_websockets: websockets.into(),
};
Ok((gateway.identity_key, description))
@@ -224,7 +224,7 @@ impl CacheItemProvider for NodeDescriptionProvider {
match res {
Ok((identity, description)) => Some((identity, description)),
Err(err) => {
debug!("{err}");
debug!("failed to obtain gateway self-described data: {err}");
None
}
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::coconut;
use nym_bin_common::bin_info;
use nym_bin_common::build_information::BinaryBuildInformation;
use okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::openapi_get_routes_spec;
use rocket_okapi::settings::OpenApiSettings;
use tokio::time::Instant;
pub(crate) mod routes;
pub(crate) struct ApiStatusState {
startup_time: Instant,
build_information: BinaryBuildInformation,
signer_information: Option<SignerState>,
}
pub(crate) struct SignerState {
// static information
pub cosmos_address: String,
pub identity: String,
pub announce_address: String,
pub(crate) coconut_keypair: coconut::keys::KeyPair,
}
impl ApiStatusState {
pub fn new() -> Self {
ApiStatusState {
startup_time: Instant::now(),
build_information: bin_info!(),
signer_information: None,
}
}
pub fn add_zk_nym_signer(&mut self, signer_information: SignerState) {
self.signer_information = Some(signer_information)
}
}
pub(crate) fn api_status_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
settings:
routes::health,
routes::build_information,
routes::signer_information
]
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::ErrorResponse;
use crate::status::ApiStatusState;
use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse};
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use nym_coconut::Base58;
use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::State;
use rocket_okapi::openapi;
#[openapi(tag = "Api Status")]
#[get("/health")]
pub(crate) async fn health(state: &State<ApiStatusState>) -> Json<ApiHealthResponse> {
let uptime = state.startup_time.elapsed();
let health = ApiHealthResponse::new_healthy(uptime);
Json(health)
}
#[openapi(tag = "Api Status")]
#[get("/build-information")]
pub(crate) async fn build_information(
state: &State<ApiStatusState>,
) -> Json<BinaryBuildInformationOwned> {
Json(state.build_information.to_owned())
}
#[openapi(tag = "Api Status")]
#[get("/signer-information")]
pub(crate) async fn signer_information(
state: &State<ApiStatusState>,
) -> Result<Json<SignerInformationResponse>, ErrorResponse> {
let signer_state = state.signer_information.as_ref().ok_or_else(|| {
ErrorResponse::new(
"this api does not expose zk-nym signing functionalities",
Status::InternalServerError,
)
})?;
Ok(Json(SignerInformationResponse {
cosmos_address: signer_state.cosmos_address.clone(),
identity: signer_state.identity.clone(),
announce_address: signer_state.announce_address.clone(),
verification_key: signer_state
.coconut_keypair
.verification_key()
.await
.map(|maybe_vk| maybe_vk.to_bs58()),
}))
}
+24 -6
View File
@@ -9,6 +9,7 @@ use crate::network::network_routes;
use crate::node_describe_cache::DescribedNodes;
use crate::node_status_api::{self, NodeStatusCache};
use crate::nym_contract_cache::cache::NymContractCache;
use crate::status::{api_status_routes, ApiStatusState, SignerState};
use crate::support::caching::cache::SharedCache;
use crate::support::config::Config;
use crate::support::{nyxd, storage};
@@ -27,7 +28,7 @@ pub(crate) mod openapi;
pub(crate) async fn setup_rocket(
config: &Config,
network_details: NetworkDetails,
_nyxd_client: nyxd::Client,
nyxd_client: nyxd::Client,
identity_keypair: identity::KeyPair,
coconut_keypair: coconut::keys::KeyPair,
) -> anyhow::Result<Rocket<Ignite>> {
@@ -45,6 +46,7 @@ pub(crate) async fn setup_rocket(
"" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings),
"/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled),
"/network" => network_routes(&openapi_settings),
"/api-status" => api_status_routes(&openapi_settings),
"" => nym_node_routes(&openapi_settings),
}
@@ -68,18 +70,34 @@ pub(crate) async fn setup_rocket(
None
};
let mut status_state = ApiStatusState::new();
let rocket = if config.coconut_signer.enabled {
// make sure we have some tokens to cover multisig fees
let balance = _nyxd_client.balance(&mix_denom).await?;
let balance = nyxd_client.balance(&mix_denom).await?;
if balance.amount < coconut::MINIMUM_BALANCE {
let address = _nyxd_client.address().await;
let address = nyxd_client.address().await;
let min = Coin::new(coconut::MINIMUM_BALANCE, mix_denom);
bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}")
}
let comm_channel = QueryCommunicationChannel::new(_nyxd_client.clone());
let cosmos_address = nyxd_client.address().await.to_string();
let announce_address = config
.coconut_signer
.announce_address
.clone()
.map(|u| u.to_string())
.unwrap_or_default();
status_state.add_zk_nym_signer(SignerState {
cosmos_address,
identity: identity_keypair.public_key().to_base58_string(),
announce_address,
coconut_keypair: coconut_keypair.clone(),
});
let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone());
rocket.attach(coconut::stage(
_nyxd_client.clone(),
nyxd_client.clone(),
mix_denom,
identity_keypair,
coconut_keypair,
@@ -97,7 +115,7 @@ pub(crate) async fn setup_rocket(
rocket
};
Ok(rocket.ignite().await?)
Ok(rocket.manage(status_state).ignite().await?)
}
fn setup_cors() -> Result<Cors> {
+3
View File
@@ -36,6 +36,9 @@ nym-bin-common = { path = "../../common/bin-common", features = ["bin_info_schem
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
rand_chacha = "0.2"
nym-crypto = { path = "../../common/crypto", features = ["rand"] }
[features]
default = ["client"]
+79 -2
View File
@@ -1,7 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::api::v1::node::models::HostInformation;
use crate::api::v1::node::models::{HostInformation, LegacyHostInformation};
use crate::error::Error;
use nym_crypto::asymmetric::identity;
use schemars::JsonSchema;
@@ -61,7 +61,16 @@ impl SignedHostInformation {
return false;
};
self.verify(&pub_key)
if self.verify(&pub_key) {
return true;
}
// attempt to verify legacy signature
SignedData {
data: LegacyHostInformation::from(self.data.clone()),
signature: self.signature.clone(),
}
.verify(&pub_key)
}
}
@@ -84,3 +93,71 @@ impl Display for ErrorResponse {
self.message.fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_crypto::asymmetric::{ed25519, x25519};
use rand_chacha::rand_core::SeedableRng;
#[test]
fn dummy_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 host_info = crate::api::v1::node::models::HostInformation {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: ed22519.public_key().to_base58_string(),
x25519_sphinx: x25519_sphinx.public_key().to_base58_string(),
x25519_noise: "".to_string(),
},
};
let signed_info = SignedHostInformation::new(host_info, ed22519.private_key()).unwrap();
assert!(signed_info.verify(ed22519.public_key()));
assert!(signed_info.verify_host_information())
}
#[test]
fn dummy_legacy_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 legacy_info = crate::api::v1::node::models::LegacyHostInformation {
ip_address: vec!["1.1.1.1".parse().unwrap()],
hostname: Some("foomp.com".to_string()),
keys: crate::api::v1::node::models::LegacyHostKeys {
ed25519: ed22519.public_key().to_base58_string(),
x25519: x25519_sphinx.public_key().to_base58_string(),
},
};
let host_info = crate::api::v1::node::models::HostInformation {
ip_address: legacy_info.ip_address.clone(),
hostname: legacy_info.hostname.clone(),
keys: crate::api::v1::node::models::HostKeys {
ed25519_identity: legacy_info.keys.ed25519.clone(),
x25519_sphinx: legacy_info.keys.x25519.clone(),
x25519_noise: "".to_string(),
},
};
// signature on legacy data
let signature = SignedData::new(legacy_info, ed22519.private_key())
.unwrap()
.signature;
// signed blob with the 'current' structure
let current_struct = SignedData {
data: host_info,
signature,
};
assert!(!current_struct.verify(ed22519.public_key()));
assert!(current_struct.verify_host_information())
}
}
@@ -7,6 +7,9 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct Gateway {
#[serde(default)]
pub enforces_zk_nyms: bool,
pub client_interfaces: ClientInterfaces,
}
@@ -32,6 +32,23 @@ pub struct HostInformation {
pub keys: HostKeys,
}
#[derive(Serialize)]
pub struct LegacyHostInformation {
pub ip_address: Vec<IpAddr>,
pub hostname: Option<String>,
pub keys: LegacyHostKeys,
}
impl From<HostInformation> for LegacyHostInformation {
fn from(value: HostInformation) -> Self {
LegacyHostInformation {
ip_address: value.ip_address,
hostname: value.hostname,
keys: value.keys.into(),
}
}
}
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct HostKeys {
@@ -49,6 +66,21 @@ pub struct HostKeys {
pub x25519_noise: String,
}
impl From<HostKeys> for LegacyHostKeys {
fn from(value: HostKeys) -> Self {
LegacyHostKeys {
ed25519: value.ed25519_identity,
x25519: value.x25519_sphinx,
}
}
}
#[derive(Serialize)]
pub struct LegacyHostKeys {
pub ed25519: String,
pub x25519: String,
}
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct HostSystem {
+6 -5
View File
@@ -18,6 +18,7 @@ pub(crate) async fn execute(mut args: Args) -> Result<(), NymNodeError> {
let config_path = args.config.config_path();
let output = args.output;
let bonding_info_path = args.bonding_information_output.clone();
let init_only = args.init_only;
let config = if !config_path.exists() {
debug!("no configuration file found at '{}'", config_path.display());
@@ -25,16 +26,11 @@ pub(crate) async fn execute(mut args: Args) -> Result<(), NymNodeError> {
if args.deny_init {
return Err(NymNodeError::ForbiddenInitialisation { config_path });
}
let init_only = args.init_only;
let maybe_custom_mnemonic = args.take_mnemonic();
let config = args.build_config()?;
NymNode::initialise(&config, maybe_custom_mnemonic).await?;
if init_only {
debug!("returning due to the 'init-only' flag");
return Ok(());
}
config
} else {
@@ -73,5 +69,10 @@ pub(crate) async fn execute(mut args: Args) -> Result<(), NymNodeError> {
})?;
}
if init_only {
debug!("returning due to the 'init-only' flag");
return Ok(());
}
nym_node.run().await
}
+11
View File
@@ -195,6 +195,14 @@ pub(crate) struct MixnetArgs {
env = NYMNODE_NYM_APIS_ARG
)]
pub(crate) nym_api_urls: Option<Vec<Url>>,
/// Addresses to nyxd chain endpoint which the node will use for chain interactions.
#[clap(
long,
value_delimiter = ',',
env = NYMNODE_NYXD_URLS_ARG
)]
pub(crate) nyxd_urls: Option<Vec<Url>>,
}
impl MixnetArgs {
@@ -210,6 +218,9 @@ impl MixnetArgs {
if let Some(nym_api_urls) = self.nym_api_urls {
section.nym_api_urls = nym_api_urls
}
if let Some(nyxd_urls) = self.nyxd_urls {
section.nyxd_urls = nyxd_urls
}
section
}
}
+1
View File
@@ -34,6 +34,7 @@ pub mod vars {
// mixnet:
pub const NYMNODE_MIXNET_BIND_ADDRESS_ARG: &str = "NYMNODE_MIXNET_BIND_ADDRESS";
pub const NYMNODE_NYM_APIS_ARG: &str = "NYMNODE_NYM_APIS";
pub const NYMNODE_NYXD_URLS_ARG: &str = "NYMNODE_NYXD";
// wireguard:
pub const NYMNODE_WG_ENABLED_ARG: &str = "NYMNODE_WG_ENABLED";
+1
View File
@@ -484,6 +484,7 @@ impl NymNode {
wss_port: self.config.entry_gateway.announce_wss_port,
});
let gateway_details = api_requests::v1::gateway::models::Gateway {
enforces_zk_nyms: self.config.entry_gateway.enforce_zk_nyms,
client_interfaces: api_requests::v1::gateway::models::ClientInterfaces {
wireguard,
mixnet_websockets,
+15 -14
View File
@@ -3,30 +3,31 @@ import os.path
import sys
def add_mixnode(base_network, base_dir, mix_id):
def add_mixnode(base_network, base_dir, mix_id, port_delta):
with open(os.path.join(base_dir, "mix" + str(mix_id) + ".json"), "r") as json_blob:
mix_data = json.load(json_blob)
base_network["mixnodes"][str(mix_id)][0]["identity_key"] = mix_data["identity_key"]
base_network["mixnodes"][str(mix_id)][0]["sphinx_key"] = mix_data["sphinx_key"]
base_network["mixnodes"][str(mix_id)][0]["mix_port"] = mix_data["mix_port"]
base_network["mixnodes"][str(mix_id)][0]["mix_port"] = 10000 + port_delta
base_network["mixnodes"][str(mix_id)][0]["version"] = mix_data["version"]
base_network["mixnodes"][str(mix_id)][0]["host"] = mix_data["bind_address"]
base_network["mixnodes"][str(mix_id)][0]["layer"] = mix_id
base_network["mixnodes"][str(mix_id)][0]["host"] = "127.0.0.1"
base_network["mixnodes"][str(mix_id)][0]["layer"] = mix_id % 3 + 1
base_network["mixnodes"][str(mix_id)][0]["mix_id"] = mix_id
base_network["mixnodes"][str(mix_id)][0]["owner"] = "whatever"
base_network["mixnodes"][str(mix_id)][0]["owner"] = "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf"
return base_network
def add_gateway(base_network, base_dir):
def add_gateway(base_network, base_dir, port_delta):
with open(os.path.join(base_dir, "gateway.json"), "r") as json_blob:
gateway_data = json.load(json_blob)
base_network["gateways"][0]["identity_key"] = gateway_data["identity_key"]
base_network["gateways"][0]["sphinx_key"] = gateway_data["sphinx_key"]
base_network["gateways"][0]["mix_port"] = gateway_data["mix_port"]
base_network["gateways"][0]["clients_port"] = gateway_data["clients_port"]
base_network["gateways"][0]["mix_port"] = 10000 + port_delta
base_network["gateways"][0]["clients_port"] = 9000
# base_network["gateways"][0]["version"] = gateway_data["version"]
base_network["gateways"][0]["host"] = gateway_data["bind_address"]
base_network["gateways"][0]["owner"] = "whatever"
base_network["gateways"][0]["host"] = "127.0.0.1"
base_network["gateways"][0]["owner"] = "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf"
return base_network
@@ -41,10 +42,10 @@ def main(args):
}
base_dir = args[0]
base_network = add_mixnode(base_network, base_dir, 1)
base_network = add_mixnode(base_network, base_dir, 2)
base_network = add_mixnode(base_network, base_dir, 3)
base_network = add_gateway(base_network, base_dir)
base_network = add_mixnode(base_network, base_dir, 1, 1)
base_network = add_mixnode(base_network, base_dir, 2, 2)
base_network = add_mixnode(base_network, base_dir, 3, 3)
base_network = add_gateway(base_network, base_dir, 4)
with open(os.path.join(base_dir, "network.json"), "w") as out:
json.dump(base_network, out, indent=2)
+8 -8
View File
@@ -20,16 +20,16 @@ echo "Using $localnetdir for the localnet"
# initialise mixnet
echo "initialising mixnode1..."
cargo run --release --bin nym-mixnode -- init --id "mix1-$suffix" --host 127.0.0.1 --mix-port 10001 --verloc-port 20001 --http-api-port 30001 --metrics-key=lala --output=json >>"$localnetdir/mix1.json"
cargo run --release --bin nym-node -- run --id "mix1-$suffix" --init-only --mixnet-bind-address=127.0.0.1:10001 --verloc-bind-address 127.0.0.1:20001 --http-bind-address 127.0.0.1:30001 --http-access-token=lala --output=json --bonding-information-output "$localnetdir/mix1.json"
echo "initialising mixnode2..."
cargo run --release --bin nym-mixnode -- init --id "mix2-$suffix" --host 127.0.0.1 --mix-port 10002 --verloc-port 20002 --http-api-port 30002 --metrics-key=lala --output=json >>"$localnetdir/mix2.json"
cargo run --release --bin nym-node -- run --id "mix2-$suffix" --init-only --mixnet-bind-address=127.0.0.1:10002 --verloc-bind-address 127.0.0.1:20002 --http-bind-address 127.0.0.1:30002 --http-access-token=lala --output=json --bonding-information-output "$localnetdir/mix2.json"
echo "initialising mixnode3..."
cargo run --release --bin nym-mixnode -- init --id "mix3-$suffix" --host 127.0.0.1 --mix-port 10003 --verloc-port 20003 --http-api-port 30003 --metrics-key=lala --output=json >>"$localnetdir/mix3.json"
cargo run --release --bin nym-node -- run --id "mix3-$suffix" --init-only --mixnet-bind-address=127.0.0.1:10003 --verloc-bind-address 127.0.0.1:20003 --http-bind-address 127.0.0.1:30003 --http-access-token=lala --output=json --bonding-information-output "$localnetdir/mix3.json"
echo "initialising gateway..."
cargo run --release --bin nym-gateway -- init --id "gateway-$suffix" --host 127.0.0.1 --mix-port 10004 --clients-port 9000 --output=json >>"$localnetdir/gateway.json"
cargo run --release --bin nym-node -- run --id "gateway-$suffix" --init-only --mode entry --mixnet-bind-address=127.0.0.1:10004 --entry-bind-address 127.0.0.1:9000 --verloc-bind-address 127.0.0.1:20004 --http-bind-address 127.0.0.1:30004 --http-access-token=lala --output=json --bonding-information-output "$localnetdir/gateway.json"
# build the topology
echo "combining json files..."
@@ -42,10 +42,10 @@ echo "the full network file is located at $networkfile"
echo "starting the mixnet..."
tmux start-server
tmux new-session -d -s localnet -n Mixnet -d "/usr/bin/env sh -c \" cargo run --release --bin nym-mixnode -- run --id mix1-$suffix \""
tmux split-window -t localnet:0 "/usr/bin/env sh -c \" cargo run --release --bin nym-mixnode -- run --id mix2-$suffix \""
tmux split-window -t localnet:0 "/usr/bin/env sh -c \" cargo run --release --bin nym-mixnode -- run --id mix3-$suffix \""
tmux split-window -t localnet:0 "/usr/bin/env sh -c \" cargo run --release --bin nym-gateway -- run --id gateway-$suffix --local \""
tmux new-session -d -s localnet -n Mixnet -d "/usr/bin/env sh -c \" cargo run --release --bin nym-node -- run --id mix1-$suffix \""
tmux split-window -t localnet:0 "/usr/bin/env sh -c \" cargo run --release --bin nym-node -- run --id mix2-$suffix \""
tmux split-window -t localnet:0 "/usr/bin/env sh -c \" cargo run --release --bin nym-node -- run --id mix3-$suffix \""
tmux split-window -t localnet:0 "/usr/bin/env sh -c \" cargo run --release --bin nym-node -- run --id gateway-$suffix \""
while ! nc -z localhost 9000; do
echo "waiting for nym-gateway to launch on port 9000..."
+4 -4
View File
@@ -1424,9 +1424,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "eyre"
version = "0.6.11"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
@@ -1841,9 +1841,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.24"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [
"bytes",
"fnv",
+4 -4
View File
@@ -1424,9 +1424,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "eyre"
version = "0.6.11"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
@@ -1841,9 +1841,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.24"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [
"bytes",
"fnv",
+4 -4
View File
@@ -1424,9 +1424,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "eyre"
version = "0.6.11"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799"
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
dependencies = [
"indenter",
"once_cell",
@@ -1841,9 +1841,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.24"
version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
dependencies = [
"bytes",
"fnv",
+2 -1
View File
@@ -39,7 +39,7 @@ mod socks5_client;
mod traits;
pub use client::{DisconnectedMixnetClient, IncludedSurbs, MixnetClientBuilder};
pub use config::{Config, KeyMode};
pub use config::Config;
pub use native_client::MixnetClient;
pub use native_client::MixnetClientSender;
pub use nym_client_core::{
@@ -77,6 +77,7 @@ pub use nym_sphinx::{
anonymous_replies::requests::AnonymousSenderTag,
receiver::ReconstructedMessage,
};
pub use nym_task::connections::TransmissionLane;
pub use nym_topology::{provider_trait::TopologyProvider, NymTopology};
pub use paths::StoragePaths;
pub use socks5_client::Socks5MixnetClient;
+117 -113
View File
@@ -10,8 +10,11 @@ use crate::NymNetworkDetails;
use crate::{Error, Result};
use futures::channel::mpsc;
use futures::StreamExt;
use log::warn;
use nym_client_core::client::base_client::storage::helpers::get_all_registered_identities;
use log::{debug, warn};
use nym_client_core::client::base_client::storage::helpers::{
get_active_gateway_identity, get_all_registered_identities, has_gateway_details,
set_active_gateway,
};
use nym_client_core::client::base_client::storage::{
Ephemeral, GatewaysDetailsStore, MixnetClientStorage, OnDiskPersistent,
};
@@ -23,6 +26,7 @@ use nym_client_core::client::{
use nym_client_core::config::DebugConfig;
use nym_client_core::error::ClientCoreError;
use nym_client_core::init::helpers::current_gateways;
use nym_client_core::init::setup_gateway;
use nym_client_core::init::types::{GatewaySelectionSpecification, GatewaySetup};
use nym_network_defaults::WG_TUN_DEVICE_IP_ADDRESS;
use nym_socks5_client_core::config::Socks5;
@@ -248,13 +252,15 @@ where
/// Construct a [`DisconnectedMixnetClient`] from the setup specified.
pub fn build(self) -> Result<DisconnectedMixnetClient<S>> {
let client = DisconnectedMixnetClient::new(self.config, self.socks5_config, self.storage)?
.custom_gateway_transceiver(self.custom_gateway_transceiver)
.custom_topology_provider(self.custom_topology_provider)
.custom_shutdown(self.custom_shutdown)
.wireguard_mode(self.wireguard_mode)
.wait_for_gateway(self.wait_for_gateway)
.force_tls(self.force_tls);
let mut client =
DisconnectedMixnetClient::new(self.config, self.socks5_config, self.storage)?;
client.custom_gateway_transceiver = self.custom_gateway_transceiver;
client.custom_topology_provider = self.custom_topology_provider;
client.custom_shutdown = self.custom_shutdown;
client.wireguard_mode = self.wireguard_mode;
client.wait_for_gateway = self.wait_for_gateway;
client.force_tls = self.force_tls;
Ok(client)
}
@@ -355,48 +361,6 @@ where
})
}
#[must_use]
pub fn custom_shutdown(mut self, shutdown: Option<TaskClient>) -> Self {
self.custom_shutdown = shutdown;
self
}
#[must_use]
pub fn custom_topology_provider(
mut self,
provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
) -> Self {
self.custom_topology_provider = provider;
self
}
#[must_use]
pub fn custom_gateway_transceiver(
mut self,
gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
) -> Self {
self.custom_gateway_transceiver = gateway_transceiver;
self
}
#[must_use]
pub fn wireguard_mode(mut self, wireguard_mode: bool) -> Self {
self.wireguard_mode = wireguard_mode;
self
}
#[must_use]
pub fn wait_for_gateway(mut self, wait_for_gateway: bool) -> Self {
self.wait_for_gateway = wait_for_gateway;
self
}
#[must_use]
pub fn force_tls(mut self, must_use_tls: bool) -> Self {
self.force_tls = must_use_tls;
self
}
fn get_api_endpoints(&self) -> Vec<Url> {
self.config
.network_details
@@ -426,6 +390,65 @@ where
}
}
async fn setup_client_keys(&self) -> Result<()> {
let mut rng = OsRng;
let key_store = self.storage.key_store();
if key_store.load_keys().await.is_err() {
debug!("Generating new client keys");
nym_client_core::init::generate_new_client_keys(&mut rng, key_store).await?;
}
Ok(())
}
async fn print_all_registered_gateway_identities(&self) {
match get_all_registered_identities(self.storage.gateway_details_store()).await {
Err(err) => {
warn!("failed to query for all registered gateways: {err}")
}
Ok(all_ids) => {
if !all_ids.is_empty() {
debug!("this client is already registered with the following gateways:");
for id in all_ids {
debug!("{id}")
}
}
}
}
}
async fn print_selected_gateway(&self) {
match self.storage.gateway_details_store().active_gateway().await {
Err(err) => {
warn!("failed to query for the current active gateway: {err}")
}
Ok(active) => {
if let Some(active) = active.registration {
let id = active.details.gateway_id();
debug!("currently selected gateway: {0}", id);
}
}
}
}
async fn set_active_gateway_if_previously_registered(
&self,
user_chosen_gateway: &str,
) -> Result<bool> {
let storage = self.storage.gateway_details_store();
// Stricly speaking, `set_active_gateway` does this check internally as well, but since the
// error is boxed away and we're using a generic storage, it's not so easy to match on it.
// This function is at least less likely to fail on something unrelated to the existence of
// the gateway in the set of registered gateways
if has_gateway_details(storage, user_chosen_gateway).await? {
set_active_gateway(storage, user_chosen_gateway).await?;
Ok(true)
} else {
Ok(false)
}
}
async fn new_gateway_setup(&self) -> Result<GatewaySetup, ClientCoreError> {
let nym_api_endpoints = self.get_api_endpoints();
@@ -445,67 +468,67 @@ where
})
}
/// Check if the client already has an active gateway enabled.
async fn has_active_gateway(&self) -> bool {
let storage = self.storage.gateway_details_store();
match storage.active_gateway().await {
Err(err) => {
warn!("failed to query for the current active gateway: {err}");
return false;
}
Ok(active) => {
if active.registration.is_some() {
return true;
}
}
}
match get_all_registered_identities(storage).await {
Err(err) => {
warn!("failed to query for all registered gateways: {err}")
}
Ok(all_ids) => {
if !all_ids.is_empty() {
warn!("this client doesn't have an active gateway set, however, it's already registered with the following gateways (consider making one of them active):");
for id in all_ids {
warn!("{id}")
}
}
}
}
false
}
/// Register with a gateway. If a gateway is provided in the config then that will try to be
/// used. If none is specified, a gateway at random will be picked.
/// used. If none is specified, a gateway at random will be picked. The used gateway is saved
/// as the active gateway.
///
/// # Errors
///
/// This function will return an error if you try to re-register when in an already registered
/// state.
pub async fn register_and_authenticate_gateway(&mut self) -> Result<()> {
pub async fn setup_gateway(&mut self) -> Result<()> {
if !matches!(self.state, BuilderState::New) {
return Err(Error::ReregisteringGatewayNotSupported);
}
log::debug!("Registering with gateway");
self.print_all_registered_gateway_identities().await;
self.print_selected_gateway().await;
let gateway_setup = if self.has_active_gateway().await {
GatewaySetup::MustLoad { gateway_id: None }
} else {
self.new_gateway_setup().await?
// Try to set active gateway to the same as the user chosen one, if it's in the set of
// gateways that is already registered.
if let Some(ref user_chosen_gateway) = self.config.user_chosen_gateway {
if self
.set_active_gateway_if_previously_registered(user_chosen_gateway)
.await?
{
debug!("user chosen gateway is already registered, set as active");
}
}
let active_gateway =
get_active_gateway_identity(self.storage.gateway_details_store()).await?;
// Determine the gateway setup based on the currently active gateway and the user-chosen
// gateway.
let gateway_setup = match (self.config.user_chosen_gateway.as_ref(), active_gateway) {
// When a user-chosen gateway exists and matches the active one.
(Some(user_chosen_gateway), Some(active_gateway))
if &active_gateway.to_base58_string() == user_chosen_gateway =>
{
GatewaySetup::MustLoad { gateway_id: None }
}
// When a user-chosen gateway exists but there's no active gateway, or it doesn't match the active one.
(Some(_), _) => self.new_gateway_setup().await?,
// When no user-chosen gateway exists but there's an active gateway.
(None, Some(_)) => GatewaySetup::MustLoad { gateway_id: None },
// When there's no user-chosen gateway and no active gateway.
(None, None) => self.new_gateway_setup().await?,
};
// this will perform necessary key and details load and optional store
let _init_result = nym_client_core::init::setup_gateway(
let init_results = setup_gateway(
gateway_setup,
self.storage.key_store(),
self.storage.gateway_details_store(),
)
.await?;
set_active_gateway(
self.storage.gateway_details_store(),
&init_results.gateway_id().to_base58_string(),
)
.await?;
self.state = BuilderState::Registered {};
Ok(())
}
@@ -527,13 +550,8 @@ where
}
async fn connect_to_mixnet_common(mut self) -> Result<(BaseClient, Recipient)> {
// if we don't care about our keys, explicitly register
if !self.config.key_mode.is_keep() {
self.register_and_authenticate_gateway().await?;
}
// otherwise, the whole key setup and gateway selection dance will be done for us
// when we start the base client
self.setup_client_keys().await?;
self.setup_gateway().await?;
let nyxd_endpoints = self.get_nyxd_endpoints();
let nym_api_endpoints = self.get_api_endpoints();
@@ -543,25 +561,11 @@ where
.config
.as_base_client_config(nyxd_endpoints, nym_api_endpoints.clone());
let known_gateway = self.has_active_gateway().await;
// if we have a known gateway, don't bother doing all of those queries
let gateway_setup = if known_gateway {
None
} else {
Some(self.new_gateway_setup().await?)
};
let mut base_builder: BaseClientBuilder<_, _> =
BaseClientBuilder::new(&base_config, self.storage, self.dkg_query_client)
.with_wait_for_gateway(self.wait_for_gateway)
.with_wireguard_connection(self.wireguard_mode);
if !known_gateway {
// safety: `gateway_setup` is always set whenever `known_gateway` is false
base_builder = base_builder.with_gateway_setup(gateway_setup.unwrap());
}
// let mut base_builder: BaseClientBuilder<_, _> = if !known_gateway {
// // we need to setup a new gateway
// let setup = self.new_gateway_setup().await;
-18
View File
@@ -5,30 +5,12 @@ use url::Url;
const DEFAULT_SDK_CLIENT_ID: &str = "_default-nym-sdk-client";
#[derive(Clone, Debug, Default)]
pub enum KeyMode {
/// Use existing key files if they exists, otherwise create new ones.
#[default]
Keep,
/// Create new keys, overwriting any potential previously existing keys.
Overwrite,
}
impl KeyMode {
pub(crate) fn is_keep(&self) -> bool {
matches!(self, KeyMode::Keep)
}
}
/// Config struct for [`crate::mixnet::MixnetClient`]
#[derive(Default)]
pub struct Config {
/// If the user has explicitly specified a gateway.
pub user_chosen_gateway: Option<String>,
/// Determines how to handle existing key files found.
pub key_mode: KeyMode,
/// The details of the network we're using. It defaults to the mainnet network.
pub network_details: NymNetworkDetails,
@@ -10,5 +10,7 @@ use nym_ip_packet_router::error::IpPacketRouterError;
pub(crate) async fn execute(
args: CommonClientImportCredentialArgs,
) -> Result<(), IpPacketRouterError> {
import_credential::<CliIpPacketRouterClient, _>(args).await
import_credential::<CliIpPacketRouterClient, _>(args).await?;
println!("successfully imported credential!");
Ok(())
}
@@ -10,5 +10,7 @@ use nym_client_core::cli_helpers::client_import_credential::{
pub(crate) async fn execute(
args: CommonClientImportCredentialArgs,
) -> Result<(), NetworkRequesterError> {
import_credential::<CliNetworkRequesterClient, _>(args).await
import_credential::<CliNetworkRequesterClient, _>(args).await?;
println!("successfully imported credential!");
Ok(())
}
@@ -444,7 +444,7 @@ impl NRServiceProvider {
controller_sender: ControllerSender,
mix_input_sender: MixProxySender<MixnetMessage>,
lane_queue_lengths: LaneQueueLengths,
shutdown: TaskClient,
mut shutdown: TaskClient,
) {
let mut conn = match socks5::tcp::Connection::new(
connection_id,
@@ -456,6 +456,7 @@ impl NRServiceProvider {
Ok(conn) => conn,
Err(err) => {
log::error!("error while connecting to {remote_addr}: {err}",);
shutdown.disarm();
// inform the remote that the connection is closed before it even was established
let mixnet_message = MixnetMessage::new_network_data_response(
@@ -23,6 +23,9 @@ pub(crate) async fn execute(
nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::DelegateVesting(args) => {
nym_cli_commands::validator::mixnet::delegators::vesting_delegate_to_mixnode::vesting_delegate_to_mixnode(args, create_signing_client(global_args, network_details)?).await
}
nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::DelegateMulti(args) => {
nym_cli_commands::validator::mixnet::delegators::delegate_to_multiple_mixnodes::delegate_to_multiple_mixnodes(args, create_signing_client(global_args, network_details)?).await
}
nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::Undelegate(args) => {
nym_cli_commands::validator::mixnet::delegators::undelegate_from_mixnode::undelegate_from_mixnode(args, create_signing_client(global_args, network_details)?).await
}