Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 407725f697 |
@@ -11,7 +11,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ arc-linux-latest-dind ]
|
||||
platform: [ arc-ubuntu-22.04 ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
@@ -28,11 +28,18 @@ jobs:
|
||||
mkdir -p $OUTPUT_DIR
|
||||
echo $OUTPUT_DIR
|
||||
|
||||
- name: Build contracts
|
||||
run: make optimize-contracts
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
|
||||
- name: Check optimized contracts
|
||||
run: make docker-check-contracts
|
||||
- name: Install cosmwasm-check
|
||||
run: cargo install cosmwasm-check
|
||||
|
||||
- name: Build release contracts
|
||||
run: make publish-contracts
|
||||
|
||||
- name: Prepare build output
|
||||
shell: bash
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
build:
|
||||
# since it's going to be compiled into wasm, there's absolutely
|
||||
# no point in running CI on different OS-es
|
||||
runs-on: arc-linux-latest
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: arc-linux-latest
|
||||
- os: arc-ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -30,13 +30,11 @@ jobs:
|
||||
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
|
||||
client_hash: ${{ steps.binary-hashes.outputs.client_hash }}
|
||||
nymvisor_hash: ${{ steps.binary-hashes.outputs.nymvisor_hash }}
|
||||
nymnode_hash: ${{ steps.binary-hashes.outputs.nymnode_hash }}
|
||||
socks5_hash: ${{ steps.binary-hashes.outputs.socks5_hash }}
|
||||
netreq_hash: ${{ steps.binary-hashes.outputs.netreq_hash }}
|
||||
cli_hash: ${{ steps.binary-hashes.outputs.cli_hash }}
|
||||
client_version: ${{ steps.binary-versions.outputs.client_version }}
|
||||
nymvisor_version: ${{ steps.binary-versions.outputs.nymvisor_version }}
|
||||
nymnode_version: ${{ steps.binary-versions.outputs.nymnode_version }}
|
||||
socks5_version: ${{ steps.binary-versions.outputs.socks5_version }}
|
||||
netreq_version: ${{ steps.binary-versions.outputs.netreq_version }}
|
||||
cli_version: ${{ steps.binary-versions.outputs.cli_version }}
|
||||
@@ -76,7 +74,6 @@ jobs:
|
||||
target/release/nym-network-requester
|
||||
target/release/nym-cli
|
||||
target/release/nymvisor
|
||||
target/release/nym-node
|
||||
retention-days: 30
|
||||
|
||||
- id: create-release
|
||||
@@ -91,7 +88,6 @@ jobs:
|
||||
target/release/nym-network-requester
|
||||
target/release/nym-cli
|
||||
target/release/nymvisor
|
||||
target/release/nym-node
|
||||
|
||||
push-release-data-client:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
|
||||
@@ -8,7 +8,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: arc-linux-latest-dind
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -8,7 +8,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: arc-linux-latest-dind
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -4,84 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.17-isabirra] (2025-09-29)
|
||||
|
||||
- Bugfix | Fix the registration handshake ([#6062])
|
||||
- Convenience for ShutdownTracker ([#6038])
|
||||
- chore: made http-api-client-macro doctest compile ([#6037])
|
||||
- feat: refresh mixnet contract on epoch progression ([#6023])
|
||||
- chore: remove legacy nodes from nym api [and kinda-ish from node status api] ([#6021])
|
||||
- Feature/credential proxy crate ([#6018])
|
||||
- Moving clients crate from vpn-client repo to here ([#6015])
|
||||
- Feature/cancellation migration ([#6014])
|
||||
- Use default value for the ports until api is deployed ([#6007])
|
||||
- bugfix: return from MixTrafficController if client request channel has closed ([#6002])
|
||||
- Revert "Create an axum_test client for more integrated unit testing (… ([#5999])
|
||||
- chore: upgraded syn to 2.0 and removed nym-execute ([#5998])
|
||||
- feat: use `ShutdownToken` (`CancellationToken` inside) for nym-api ([#5997])
|
||||
- bugfix: Recipient deserialisation for deserialisers missing bytes specialisation ([#5991])
|
||||
- chore: use updated version of simulate endpoint ([#5988])
|
||||
- chore: purge temp databases on build ([#5984])
|
||||
- Bump sha.js from 2.4.11 to 2.4.12 ([#5983])
|
||||
- Feature: Delegation program stake checker and adjuster ([#5980])
|
||||
- build(deps): bump actions/setup-java from 4 to 5 ([#5975])
|
||||
- Domain fronting integration ([#5974])
|
||||
- chore: internal hidden command to force advance nyx epoch ([#5964])
|
||||
- Create an axum_test client for more integrated unit testing ([#5956])
|
||||
- feat: shared library for attempting to retrieve update mode attestation ([#5954])
|
||||
- Bump slab from 0.4.10 to 0.4.11 ([#5952])
|
||||
- build(deps): bump actions/first-interaction from 1 to 3 ([#5950])
|
||||
- fix: use WASM compatible time API in client ([#5948])
|
||||
- feat: credential proxy deposit pool ([#5945])
|
||||
- build(deps): bump actions/download-artifact from 4 to 5 ([#5939])
|
||||
- feat: nym signers monitor ([#5933])
|
||||
- Bump console from 0.15.11 to 0.16.0 ([#5931])
|
||||
- Bump mock_instant from 0.5.3 to 0.6.0 ([#5930])
|
||||
- Bump tokio from 1.46.1 to 1.47.1 ([#5929])
|
||||
- Bump defguard_wireguard_rs from v0.4.7 to v0.7.5 ([#5928])
|
||||
- Bump indicatif from 0.17.11 to 0.18.0 ([#5924])
|
||||
- Feature: Nym node autorun CLI ([#5916])
|
||||
- build(deps): bump mikefarah/yq from 4.45.4 to 4.47.1 ([#5911])
|
||||
- build(deps): bump pbkdf2 from 3.1.2 to 3.1.3 ([#5869])
|
||||
|
||||
[#6062]: https://github.com/nymtech/nym/pull/6062
|
||||
[#6038]: https://github.com/nymtech/nym/pull/6038
|
||||
[#6037]: https://github.com/nymtech/nym/pull/6037
|
||||
[#6023]: https://github.com/nymtech/nym/pull/6023
|
||||
[#6021]: https://github.com/nymtech/nym/pull/6021
|
||||
[#6018]: https://github.com/nymtech/nym/pull/6018
|
||||
[#6015]: https://github.com/nymtech/nym/pull/6015
|
||||
[#6014]: https://github.com/nymtech/nym/pull/6014
|
||||
[#6007]: https://github.com/nymtech/nym/pull/6007
|
||||
[#6002]: https://github.com/nymtech/nym/pull/6002
|
||||
[#5999]: https://github.com/nymtech/nym/pull/5999
|
||||
[#5998]: https://github.com/nymtech/nym/pull/5998
|
||||
[#5997]: https://github.com/nymtech/nym/pull/5997
|
||||
[#5991]: https://github.com/nymtech/nym/pull/5991
|
||||
[#5988]: https://github.com/nymtech/nym/pull/5988
|
||||
[#5984]: https://github.com/nymtech/nym/pull/5984
|
||||
[#5983]: https://github.com/nymtech/nym/pull/5983
|
||||
[#5980]: https://github.com/nymtech/nym/pull/5980
|
||||
[#5975]: https://github.com/nymtech/nym/pull/5975
|
||||
[#5974]: https://github.com/nymtech/nym/pull/5974
|
||||
[#5964]: https://github.com/nymtech/nym/pull/5964
|
||||
[#5956]: https://github.com/nymtech/nym/pull/5956
|
||||
[#5954]: https://github.com/nymtech/nym/pull/5954
|
||||
[#5952]: https://github.com/nymtech/nym/pull/5952
|
||||
[#5950]: https://github.com/nymtech/nym/pull/5950
|
||||
[#5948]: https://github.com/nymtech/nym/pull/5948
|
||||
[#5945]: https://github.com/nymtech/nym/pull/5945
|
||||
[#5939]: https://github.com/nymtech/nym/pull/5939
|
||||
[#5933]: https://github.com/nymtech/nym/pull/5933
|
||||
[#5931]: https://github.com/nymtech/nym/pull/5931
|
||||
[#5930]: https://github.com/nymtech/nym/pull/5930
|
||||
[#5929]: https://github.com/nymtech/nym/pull/5929
|
||||
[#5928]: https://github.com/nymtech/nym/pull/5928
|
||||
[#5924]: https://github.com/nymtech/nym/pull/5924
|
||||
[#5916]: https://github.com/nymtech/nym/pull/5916
|
||||
[#5911]: https://github.com/nymtech/nym/pull/5911
|
||||
[#5869]: https://github.com/nymtech/nym/pull/5869
|
||||
|
||||
## [2025.16-halloumi] (2025-09-16)
|
||||
|
||||
- Backport metadata endpoint ([#6010])
|
||||
|
||||
Generated
+3
-1
@@ -4926,11 +4926,13 @@ dependencies = [
|
||||
"nym-bandwidth-controller",
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-pemstore",
|
||||
"nym-registration-common",
|
||||
"nym-sdk",
|
||||
"nym-service-provider-requests-common",
|
||||
"nym-validator-client",
|
||||
"nym-wireguard-types",
|
||||
"rand 0.8.5",
|
||||
"semver 1.0.26",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
@@ -5351,7 +5353,6 @@ dependencies = [
|
||||
"cosmwasm-std",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"nym-contracts-common",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
@@ -6671,6 +6672,7 @@ dependencies = [
|
||||
name = "nym-registration-client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"nym-authenticator-client",
|
||||
"nym-bandwidth-controller",
|
||||
"nym-credential-storage",
|
||||
|
||||
@@ -154,7 +154,6 @@ CONTRACTS_OUT_DIR = contracts/artifacts
|
||||
#
|
||||
COSMWASM_OPTIMIZER_IMAGE ?= cosmwasm/optimizer:0.17.0
|
||||
COSMWASM_OPTIMIZER_PLATFORM ?= linux/amd64
|
||||
COSMWASM_CHECK_IMAGE ?= rust:1.88
|
||||
|
||||
# Ensure clean build environment and run the optimizer
|
||||
optimize-contracts:
|
||||
@@ -180,13 +179,6 @@ optimize-contracts:
|
||||
# Cleanup temporary artefacts directory
|
||||
@rm -rf artifacts 2>/dev/null || true
|
||||
|
||||
# Check artifacts with cosmwasm-check inside the optimizer image
|
||||
docker-check-contracts:
|
||||
@docker run --rm --platform $(COSMWASM_OPTIMIZER_PLATFORM) \
|
||||
-v $(CURDIR):/code --workdir /code \
|
||||
--entrypoint /bin/sh \
|
||||
$(COSMWASM_CHECK_IMAGE) -lc 'apt-get update && apt-get install -y --no-install-recommends llvm-dev libclang-dev pkg-config && export PATH="/usr/local/cargo/bin:/usr/local/rustup/bin:$$PATH" && cargo install cosmwasm-check --locked && WASMER_ENGINE=universal WASMER_COMPILER=singlepass cosmwasm-check contracts/artifacts/*.wasm'
|
||||
|
||||
wasm-opt-contracts:
|
||||
@for WASM in $(WASM_CONTRACT_DIR)/*.wasm; do \
|
||||
echo "Running wasm-opt on $$WASM"; \
|
||||
|
||||
@@ -7,11 +7,12 @@ use super::statistics_control::StatisticsControl;
|
||||
use crate::client::base_client::storage::helpers::store_client_keys;
|
||||
use crate::client::base_client::storage::MixnetClientStorage;
|
||||
use crate::client::cover_traffic_stream::LoopCoverTrafficStream;
|
||||
use crate::client::event_control::EventControl;
|
||||
use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender};
|
||||
use crate::client::key_manager::persistence::KeyStore;
|
||||
use crate::client::key_manager::ClientKeys;
|
||||
use crate::client::mix_traffic::transceiver::{GatewayReceiver, GatewayTransceiver, RemoteGateway};
|
||||
use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController};
|
||||
use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController, MixTrafficEvent};
|
||||
use crate::client::real_messages_control;
|
||||
use crate::client::real_messages_control::RealMessagesController;
|
||||
use crate::client::received_buffer::{
|
||||
@@ -66,7 +67,6 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::*;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(all(
|
||||
@@ -79,6 +79,23 @@ pub mod non_wasm_helpers;
|
||||
pub mod helpers;
|
||||
pub mod storage;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum MixnetClientEvent {
|
||||
Traffic(MixTrafficEvent),
|
||||
}
|
||||
|
||||
pub type EventReceiver = mpsc::UnboundedReceiver<MixnetClientEvent>;
|
||||
#[derive(Clone)]
|
||||
pub struct EventSender(pub mpsc::UnboundedSender<MixnetClientEvent>);
|
||||
|
||||
impl EventSender {
|
||||
pub fn send(&self, event: MixnetClientEvent) {
|
||||
if let Err(err) = self.0.unbounded_send(event) {
|
||||
tracing::warn!("Failed to send error event. The caller event reader was closed: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClientInput {
|
||||
pub connection_command_sender: ConnectionCommandSender,
|
||||
@@ -194,6 +211,7 @@ pub struct BaseClientBuilder<C, S: MixnetClientStorage> {
|
||||
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
|
||||
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send>>,
|
||||
shutdown: Option<ShutdownTracker>,
|
||||
event_tx: Option<EventSender>,
|
||||
user_agent: Option<UserAgent>,
|
||||
|
||||
setup_method: GatewaySetup,
|
||||
@@ -222,6 +240,7 @@ where
|
||||
custom_topology_provider: None,
|
||||
custom_gateway_transceiver: None,
|
||||
shutdown: None,
|
||||
event_tx: None,
|
||||
user_agent: None,
|
||||
setup_method: GatewaySetup::MustLoad { gateway_id: None },
|
||||
#[cfg(unix)]
|
||||
@@ -284,6 +303,12 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_event_tx(mut self, event_tx: EventSender) -> Self {
|
||||
self.event_tx = Some(event_tx);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_user_agent(mut self, user_agent: UserAgent) -> Self {
|
||||
self.user_agent = Some(user_agent);
|
||||
@@ -314,6 +339,18 @@ where
|
||||
details.client_address()
|
||||
}
|
||||
|
||||
fn start_event_control(
|
||||
parent_event_tx: Option<EventSender>,
|
||||
children_event_rx: EventReceiver,
|
||||
shutdown_tracker: &ShutdownTracker,
|
||||
) {
|
||||
let event_control = EventControl::new(parent_event_tx, children_event_rx);
|
||||
shutdown_tracker.try_spawn_named_with_shutdown(
|
||||
async move { event_control.run().await },
|
||||
"EventControl",
|
||||
);
|
||||
}
|
||||
|
||||
// future constantly pumping loop cover traffic at some specified average rate
|
||||
// the pumped traffic goes to the MixTrafficController
|
||||
fn start_cover_traffic_stream(
|
||||
@@ -325,7 +362,7 @@ where
|
||||
stats_tx: ClientStatsSender,
|
||||
shutdown_tracker: &ShutdownTracker,
|
||||
) {
|
||||
info!("Starting loop cover traffic stream...");
|
||||
tracing::info!("Starting loop cover traffic stream...");
|
||||
|
||||
let mut stream = LoopCoverTrafficStream::new(
|
||||
ack_key,
|
||||
@@ -357,7 +394,7 @@ where
|
||||
stats_tx: ClientStatsSender,
|
||||
shutdown_tracker: &ShutdownTracker,
|
||||
) {
|
||||
info!("Starting real traffic stream...");
|
||||
tracing::info!("Starting real traffic stream...");
|
||||
|
||||
let real_messages_controller = RealMessagesController::new(
|
||||
controller_config,
|
||||
@@ -442,7 +479,7 @@ where
|
||||
metrics_reporter: ClientStatsSender,
|
||||
shutdown_tracker: &ShutdownTracker,
|
||||
) {
|
||||
info!("Starting received messages buffer controller...");
|
||||
tracing::info!("Starting received messages buffer controller...");
|
||||
let controller = ReceivedMessagesBufferController::<SphinxMessageReceiver>::new(
|
||||
local_encryption_keypair,
|
||||
query_receiver,
|
||||
@@ -553,7 +590,7 @@ where
|
||||
details_store
|
||||
.upgrade_stored_remote_gateway_key(gateway_client.gateway_identity(), &updated_key)
|
||||
.await.map_err(|err| {
|
||||
error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}");
|
||||
tracing::error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}");
|
||||
ClientCoreError::GatewaysDetailsStoreError { source: Box::new(err) }
|
||||
})?
|
||||
}
|
||||
@@ -650,7 +687,7 @@ where
|
||||
|
||||
if topology_config.disable_refreshing {
|
||||
// if we're not spawning the refresher, don't cause shutdown immediately
|
||||
info!("The background topology refresher is not going to be started");
|
||||
tracing::info!("The background topology refresher is not going to be started");
|
||||
}
|
||||
|
||||
let mut topology_refresher = TopologyRefresher::new(
|
||||
@@ -660,7 +697,7 @@ where
|
||||
);
|
||||
// before returning, block entire runtime to refresh the current network view so that any
|
||||
// components depending on topology would see a non-empty view
|
||||
info!("Obtaining initial network topology");
|
||||
tracing::info!("Obtaining initial network topology");
|
||||
topology_refresher.try_refresh().await;
|
||||
|
||||
if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
|
||||
@@ -686,13 +723,13 @@ where
|
||||
.wait_for_gateway(local_gateway, waiting_timeout)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
tracing::error!(
|
||||
"the gateway did not come back online within the specified timeout: {err}"
|
||||
);
|
||||
return Err(err.into());
|
||||
}
|
||||
} else {
|
||||
error!("the gateway we're supposedly connected to does not exist. We'll not be able to send any packets to ourselves: {err}");
|
||||
tracing::error!("the gateway we're supposedly connected to does not exist. We'll not be able to send any packets to ourselves: {err}");
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
@@ -700,7 +737,7 @@ where
|
||||
if !topology_config.disable_refreshing {
|
||||
// don't spawn the refresher if we don't want to be refreshing the topology.
|
||||
// only use the initial values obtained
|
||||
info!("Starting topology refresher...");
|
||||
tracing::info!("Starting topology refresher...");
|
||||
shutdown_tracker.try_spawn_named_with_shutdown(
|
||||
async move { topology_refresher.run().await },
|
||||
"TopologyRefresher",
|
||||
@@ -717,7 +754,7 @@ where
|
||||
input_sender: Sender<InputMessage>,
|
||||
shutdown_tracker: &ShutdownTracker,
|
||||
) -> ClientStatsSender {
|
||||
info!("Starting statistics control...");
|
||||
tracing::info!("Starting statistics control...");
|
||||
StatisticsControl::create_and_start(
|
||||
config.debug.stats_reporting,
|
||||
user_agent
|
||||
@@ -732,10 +769,14 @@ where
|
||||
fn start_mix_traffic_controller(
|
||||
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
|
||||
shutdown_tracker: &ShutdownTracker,
|
||||
event_tx: EventSender,
|
||||
) -> (BatchMixMessageSender, ClientRequestSender) {
|
||||
info!("Starting mix traffic controller...");
|
||||
let (mut mix_traffic_controller, mix_tx, client_tx) =
|
||||
MixTrafficController::new(gateway_transceiver, shutdown_tracker.clone_shutdown_token());
|
||||
tracing::info!("Starting mix traffic controller...");
|
||||
let (mut mix_traffic_controller, mix_tx, client_tx) = MixTrafficController::new(
|
||||
gateway_transceiver,
|
||||
shutdown_tracker.clone_shutdown_token(),
|
||||
event_tx,
|
||||
);
|
||||
|
||||
shutdown_tracker.try_spawn_named(
|
||||
async move { mix_traffic_controller.run().await },
|
||||
@@ -799,7 +840,7 @@ where
|
||||
{
|
||||
// if client keys do not exist already, create and persist them
|
||||
if key_store.load_keys().await.is_err() {
|
||||
info!("could not find valid client keys - a new set will be generated");
|
||||
tracing::info!("could not find valid client keys - a new set will be generated");
|
||||
let mut rng = OsRng;
|
||||
let keys = if let Some(derivation_material) = derivation_material {
|
||||
ClientKeys::from_master_key(&mut rng, &derivation_material)
|
||||
@@ -846,7 +887,7 @@ where
|
||||
<S::CredentialStore as CredentialStorage>::StorageError: Send + Sync + 'static,
|
||||
<S::GatewaysDetailsStore as GatewaysDetailsStore>::StorageError: Sync + Send,
|
||||
{
|
||||
info!("Starting nym client");
|
||||
tracing::info!("Starting nym client");
|
||||
|
||||
// derive (or load) client keys and gateway configuration
|
||||
let init_res = Self::initialise_keys_and_gateway(
|
||||
@@ -875,6 +916,9 @@ where
|
||||
// channels responsible for controlling real messages
|
||||
let (input_sender, input_receiver) = tokio::sync::mpsc::channel::<InputMessage>(1);
|
||||
|
||||
// channels responsible for event management
|
||||
let (event_sender, event_receiver) = mpsc::unbounded();
|
||||
|
||||
// channels responsible for controlling ack messages
|
||||
let (ack_sender, ack_receiver) = mpsc::unbounded();
|
||||
let shared_topology_accessor =
|
||||
@@ -887,6 +931,8 @@ where
|
||||
None => nym_task::get_sdk_shutdown_tracker()?,
|
||||
};
|
||||
|
||||
Self::start_event_control(self.event_tx, event_receiver, &shutdown_tracker);
|
||||
|
||||
// channels responsible for dealing with reply-related fun
|
||||
let (reply_controller_sender, reply_controller_receiver) =
|
||||
reply_controller::requests::new_control_channels();
|
||||
@@ -977,6 +1023,7 @@ where
|
||||
let (message_sender, client_request_sender) = Self::start_mix_traffic_controller(
|
||||
gateway_transceiver,
|
||||
&shutdown_tracker.child_tracker(),
|
||||
EventSender(event_sender),
|
||||
);
|
||||
|
||||
// Channels that the websocket listener can use to signal downstream to the real traffic
|
||||
@@ -1026,8 +1073,8 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
debug!("Core client startup finished!");
|
||||
debug!("The address of this client is: {self_address}");
|
||||
tracing::debug!("Core client startup finished!");
|
||||
tracing::debug!("The address of this client is: {self_address}");
|
||||
|
||||
Ok(BaseClient {
|
||||
address: self_address,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use futures::StreamExt;
|
||||
|
||||
use crate::client::base_client::{EventReceiver, EventSender, MixnetClientEvent};
|
||||
|
||||
/// Launches and manages task events, propagating upwards what is not strictly internal.
|
||||
pub(crate) struct EventControl {
|
||||
parent_event_tx: Option<EventSender>,
|
||||
children_event_rx: EventReceiver,
|
||||
}
|
||||
|
||||
impl EventControl {
|
||||
pub(crate) fn new(
|
||||
parent_event_tx: Option<EventSender>,
|
||||
children_event_rx: EventReceiver,
|
||||
) -> Self {
|
||||
EventControl {
|
||||
parent_event_tx,
|
||||
children_event_rx,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_internal(event: MixnetClientEvent) -> bool {
|
||||
match event {
|
||||
MixnetClientEvent::Traffic(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run(mut self) {
|
||||
while let Some(event) = self.children_event_rx.next().await {
|
||||
if let Some(parent_event_tx) = &self.parent_event_tx {
|
||||
if !Self::is_internal(event) {
|
||||
parent_event_tx.send(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::mix_traffic::transceiver::GatewayTransceiver;
|
||||
use crate::client::{
|
||||
base_client::{EventSender, MixnetClientEvent},
|
||||
mix_traffic::transceiver::GatewayTransceiver,
|
||||
};
|
||||
use nym_gateway_requests::ClientRequest;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_task::ShutdownToken;
|
||||
@@ -22,6 +25,11 @@ const MAX_FAILURE_COUNT: usize = 100;
|
||||
// that's also disgusting.
|
||||
pub struct Empty;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum MixTrafficEvent {
|
||||
FailedSendingSphinx,
|
||||
}
|
||||
|
||||
pub struct MixTrafficController {
|
||||
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
|
||||
|
||||
@@ -33,12 +41,14 @@ pub struct MixTrafficController {
|
||||
consecutive_gateway_failure_count: usize,
|
||||
|
||||
shutdown_token: ShutdownToken,
|
||||
event_tx: EventSender,
|
||||
}
|
||||
|
||||
impl MixTrafficController {
|
||||
pub fn new<T>(
|
||||
gateway_transceiver: T,
|
||||
shutdown_token: ShutdownToken,
|
||||
event_tx: EventSender,
|
||||
) -> (
|
||||
MixTrafficController,
|
||||
BatchMixMessageSender,
|
||||
@@ -59,6 +69,7 @@ impl MixTrafficController {
|
||||
client_rx: client_receiver,
|
||||
consecutive_gateway_failure_count: 0,
|
||||
shutdown_token,
|
||||
event_tx,
|
||||
},
|
||||
message_sender,
|
||||
client_sender,
|
||||
@@ -68,6 +79,7 @@ impl MixTrafficController {
|
||||
pub fn new_dynamic(
|
||||
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
|
||||
shutdown_token: ShutdownToken,
|
||||
event_tx: EventSender,
|
||||
) -> (
|
||||
MixTrafficController,
|
||||
BatchMixMessageSender,
|
||||
@@ -83,6 +95,7 @@ impl MixTrafficController {
|
||||
client_rx: client_receiver,
|
||||
consecutive_gateway_failure_count: 0,
|
||||
shutdown_token,
|
||||
event_tx,
|
||||
},
|
||||
message_sender,
|
||||
client_sender,
|
||||
@@ -155,6 +168,7 @@ impl MixTrafficController {
|
||||
error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead");
|
||||
// Do we need to handle the embedded mixnet client case
|
||||
// separately?
|
||||
self.event_tx.send(MixnetClientEvent::Traffic(MixTrafficEvent::FailedSendingSphinx));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
pub mod base_client;
|
||||
pub mod cover_traffic_stream;
|
||||
pub(crate) mod event_control;
|
||||
pub(crate) mod helpers;
|
||||
pub mod inbound_messages;
|
||||
pub mod key_manager;
|
||||
|
||||
@@ -136,27 +136,6 @@ pub trait DkgSigningClient {
|
||||
self.execute_dkg_contract(fee, req, "trigger DKG resharing".to_string(), vec![])
|
||||
.await
|
||||
}
|
||||
|
||||
async fn transfer_ownership(
|
||||
&self,
|
||||
transfer_to: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = DkgExecuteMsg::TransferOwnership { transfer_to };
|
||||
|
||||
self.execute_dkg_contract(fee, req, "".to_string(), vec![])
|
||||
.await
|
||||
}
|
||||
async fn update_announce_address(
|
||||
&self,
|
||||
new_address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = DkgExecuteMsg::UpdateAnnounceAddress { new_address };
|
||||
|
||||
self.execute_dkg_contract(fee, req, "".to_string(), vec![])
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -189,7 +168,6 @@ where
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_coconut_dkg_common::msg::ExecuteMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
@@ -232,12 +210,6 @@ mod tests {
|
||||
DkgExecuteMsg::AdvanceEpochState {} => client.advance_dkg_epoch_state(None).ignore(),
|
||||
DkgExecuteMsg::TriggerReset {} => client.trigger_dkg_reset(None).ignore(),
|
||||
DkgExecuteMsg::TriggerResharing {} => client.trigger_dkg_resharing(None).ignore(),
|
||||
ExecuteMsg::TransferOwnership { transfer_to } => {
|
||||
client.transfer_ownership(transfer_to, None).ignore()
|
||||
}
|
||||
ExecuteMsg::UpdateAnnounceAddress { new_address } => {
|
||||
client.update_announce_address(new_address, None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,6 @@ use crate::types::{EncodedBTEPublicKeyWithProof, NodeIndex};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::Addr;
|
||||
|
||||
pub type BlockHeight = u64;
|
||||
pub type TransactionIndex = u32;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct OwnershipTransfer {
|
||||
pub node_index: NodeIndex,
|
||||
pub from: Addr,
|
||||
pub to: Addr,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct DealerDetails {
|
||||
pub address: Addr,
|
||||
|
||||
@@ -73,17 +73,6 @@ pub enum ExecuteMsg {
|
||||
TriggerReset {},
|
||||
|
||||
TriggerResharing {},
|
||||
|
||||
/// Transfers ownership of the epoch dealer to another address.
|
||||
/// This assumes off-chain hand-over of keys
|
||||
TransferOwnership {
|
||||
transfer_to: String,
|
||||
},
|
||||
|
||||
/// Update announce address of this signer
|
||||
UpdateAnnounceAddress {
|
||||
new_address: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
|
||||
@@ -20,7 +20,5 @@ rand_chacha = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
cw-multi-test = { workspace = true }
|
||||
|
||||
nym-contracts-common = { path = "../contracts-common" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -3,98 +3,18 @@
|
||||
|
||||
use cosmwasm_std::testing::{message_info, MockApi, MockQuerier, MockStorage};
|
||||
use cosmwasm_std::{
|
||||
coins, Addr, BankMsg, CosmosMsg, Decimal, Empty, Env, MemoryStorage, MessageInfo, Order,
|
||||
OwnedDeps, Response, StdResult, Storage,
|
||||
coins, Addr, BankMsg, CosmosMsg, Empty, Env, MemoryStorage, MessageInfo, Order, OwnedDeps,
|
||||
Response, StdResult, Storage,
|
||||
};
|
||||
use cw_storage_plus::{KeyDeserialize, Map, Prefix, PrimaryKey};
|
||||
use nym_contracts_common::events::may_find_attribute;
|
||||
use rand::{RngCore, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::fmt::Debug;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub const TEST_DENOM: &str = "unym";
|
||||
pub const TEST_PREFIX: &str = "n";
|
||||
|
||||
pub trait FindAttribute {
|
||||
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>;
|
||||
|
||||
fn any_attribute(&self, attribute: &str) -> String {
|
||||
self.attribute::<_, String>(None, attribute)
|
||||
}
|
||||
|
||||
fn any_parsed_attribute<T>(&self, attribute: &str) -> T
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Debug,
|
||||
{
|
||||
self.parsed_attribute::<_, String, T>(None, attribute)
|
||||
}
|
||||
|
||||
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Debug;
|
||||
|
||||
fn decimal<E, S>(&self, event_type: E, attribute: &str) -> Decimal
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
{
|
||||
self.parsed_attribute(event_type, attribute)
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn find_attribute<S: Into<String>>(
|
||||
event_type: Option<S>,
|
||||
attribute: &str,
|
||||
response: &Response,
|
||||
) -> String {
|
||||
let event_type = event_type.map(Into::into);
|
||||
for event in &response.events {
|
||||
if let Some(typ) = &event_type {
|
||||
if &event.ty != typ {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(attr) = may_find_attribute(event, attribute) {
|
||||
return attr;
|
||||
}
|
||||
}
|
||||
// this is only used in tests so panic here is fine
|
||||
panic!("did not find the attribute")
|
||||
}
|
||||
|
||||
impl FindAttribute for Response {
|
||||
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
{
|
||||
find_attribute(event_type.into(), attribute, self)
|
||||
}
|
||||
|
||||
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Debug,
|
||||
{
|
||||
find_attribute(event_type.into(), attribute, self)
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mock_api() -> MockApi {
|
||||
MockApi::default().with_prefix(TEST_PREFIX)
|
||||
}
|
||||
|
||||
+9
-41
@@ -4,8 +4,8 @@
|
||||
use crate::{ContractTester, TestableNymContract};
|
||||
use cosmwasm_std::testing::{message_info, mock_env};
|
||||
use cosmwasm_std::{
|
||||
from_json, Addr, BlockInfo, Coin, ContractInfo, Deps, DepsMut, Env, MessageInfo, Response,
|
||||
StdResult, Storage, Timestamp,
|
||||
from_json, Addr, Coin, ContractInfo, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
|
||||
Storage, Timestamp,
|
||||
};
|
||||
use cw_multi_test::{next_block, AppResponse, Executor};
|
||||
use serde::de::DeserializeOwned;
|
||||
@@ -62,8 +62,6 @@ pub trait ContractOpts {
|
||||
coins: &[Coin],
|
||||
message: Self::ExecuteMsg,
|
||||
) -> Result<Response, Self::ContractError>;
|
||||
|
||||
fn unchecked_contract_address<D: TestableNymContract>(&self) -> Addr;
|
||||
}
|
||||
|
||||
impl<C> ContractOpts for ContractTester<C>
|
||||
@@ -132,47 +130,14 @@ where
|
||||
|
||||
C::execute()(self.deps_mut(), env, info, message)
|
||||
}
|
||||
|
||||
fn unchecked_contract_address<D: TestableNymContract>(&self) -> Addr {
|
||||
self.unchecked_contract_address::<D>()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ChainOpts: ContractOpts {
|
||||
fn set_contract_balance(&mut self, balance: Coin);
|
||||
|
||||
fn update_block<F: Fn(&mut BlockInfo)>(&mut self, action: F);
|
||||
fn set_to_epoch(&mut self) {
|
||||
self.set_block_time(Timestamp::from_seconds(0))
|
||||
}
|
||||
fn next_block(&mut self);
|
||||
|
||||
fn set_to_genesis(&mut self) {
|
||||
self.update_block(|block| {
|
||||
block.height = 1;
|
||||
})
|
||||
}
|
||||
|
||||
fn next_block(&mut self) {
|
||||
self.update_block(next_block)
|
||||
}
|
||||
|
||||
fn advance_day_of_blocks(&mut self) {
|
||||
self.update_block(|block| {
|
||||
block.time = block.time.plus_seconds(24 * 60 * 60);
|
||||
block.height += 17280;
|
||||
})
|
||||
}
|
||||
|
||||
fn advance_time_by(&mut self, delta_secs: u64) {
|
||||
self.update_block(|block| {
|
||||
block.time = block.time.plus_seconds(delta_secs);
|
||||
block.height += 1
|
||||
})
|
||||
}
|
||||
|
||||
fn set_block_time(&mut self, time: Timestamp) {
|
||||
self.update_block(|b| b.time = time)
|
||||
}
|
||||
fn set_block_time(&mut self, time: Timestamp);
|
||||
|
||||
fn execute_msg(
|
||||
&mut self,
|
||||
@@ -221,9 +186,12 @@ where
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
fn next_block(&mut self) {
|
||||
self.app.update_block(next_block)
|
||||
}
|
||||
|
||||
fn update_block<F: Fn(&mut BlockInfo)>(&mut self, action: F) {
|
||||
self.app.update_block(action)
|
||||
fn set_block_time(&mut self, time: Timestamp) {
|
||||
self.app.update_block(|b| b.time = time)
|
||||
}
|
||||
|
||||
fn execute_msg(
|
||||
|
||||
@@ -11,14 +11,13 @@ use cosmwasm_std::{
|
||||
};
|
||||
use cw_multi_test::Executor;
|
||||
use cw_storage_plus::{Key, Path, PrimaryKey};
|
||||
use rand::RngCore;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::any::type_name;
|
||||
use std::ops::Deref;
|
||||
|
||||
pub use rand::prelude::*;
|
||||
|
||||
pub trait StorageReader {
|
||||
fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]>;
|
||||
|
||||
|
||||
@@ -47,11 +47,27 @@ pub trait TestableNymContract {
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError>;
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError>;
|
||||
|
||||
// not all instances will require default init message, some will always have to provide customised values
|
||||
#[allow(clippy::unimplemented)]
|
||||
fn base_init_msg() -> Self::InitMsg {
|
||||
unimplemented!("attempted to instantiate contract without defining instantiate message")
|
||||
}
|
||||
fn base_init_msg() -> Self::InitMsg;
|
||||
|
||||
// // for now we don't care about custom queriers
|
||||
// fn contract_wrapper() -> ContractWrapper<
|
||||
// Self::ExecuteMsg,
|
||||
// Self::InitMsg,
|
||||
// Self::QueryMsg,
|
||||
// Self::ContractError,
|
||||
// anyhow::Error,
|
||||
// anyhow::Error,
|
||||
// Empty,
|
||||
// Empty,
|
||||
// Empty,
|
||||
// Self::ContractError,
|
||||
// Self::ContractError,
|
||||
// Self::MigrateMsg,
|
||||
// Self::ContractError,
|
||||
// > {
|
||||
// ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query())
|
||||
// .with_migrate(Self::migrate())
|
||||
// }
|
||||
|
||||
fn dyn_contract() -> Box<dyn Contract<Empty>> {
|
||||
Box::new(
|
||||
@@ -76,7 +92,6 @@ pub struct ContractTesterBuilder<C> {
|
||||
app: App<BankKeeper, MockApi, StorageWrapper>,
|
||||
storage: StorageWrapper,
|
||||
pub well_known_contracts: HashMap<&'static str, Addr>,
|
||||
code_ids: HashMap<&'static str, u64>,
|
||||
}
|
||||
|
||||
impl<C> ContractTesterBuilder<C> {
|
||||
@@ -110,33 +125,20 @@ impl<C> ContractTesterBuilder<C> {
|
||||
app,
|
||||
storage,
|
||||
well_known_contracts: Default::default(),
|
||||
code_ids: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn master_address(&self) -> Addr {
|
||||
self.master_address.clone()
|
||||
}
|
||||
|
||||
pub fn instantiate<D: TestableNymContract>(
|
||||
mut self,
|
||||
custom_init_msg: Option<D::InitMsg>,
|
||||
) -> ContractTesterBuilder<C> {
|
||||
self.instantiate_contract::<D>(custom_init_msg);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn instantiate_contract<D: TestableNymContract>(
|
||||
&mut self,
|
||||
custom_init_msg: Option<D::InitMsg>,
|
||||
) {
|
||||
let code_id = self.app.store_code(D::dyn_contract());
|
||||
let contract_address = self
|
||||
.app
|
||||
.instantiate_contract(
|
||||
code_id,
|
||||
self.master_address.clone(),
|
||||
&custom_init_msg.unwrap_or_else(|| D::base_init_msg()),
|
||||
&custom_init_msg.unwrap_or(D::base_init_msg()),
|
||||
&[],
|
||||
D::NAME,
|
||||
Some(self.master_address.to_string()),
|
||||
@@ -152,28 +154,8 @@ impl<C> ContractTesterBuilder<C> {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.code_ids.insert(D::NAME, code_id);
|
||||
self.well_known_contracts.insert(D::NAME, contract_address);
|
||||
}
|
||||
|
||||
// uses the SAME code
|
||||
pub fn migrate_contract<D: TestableNymContract>(&mut self, migrate_msg: &D::MigrateMsg) {
|
||||
self.app
|
||||
.migrate_contract(
|
||||
self.master_address.clone(),
|
||||
self.unchecked_contract_address::<D>(),
|
||||
migrate_msg,
|
||||
self.unchecked_contract_code_id::<D>(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn unchecked_contract_address<D: TestableNymContract>(&self) -> Addr {
|
||||
self.well_known_contracts.get(D::NAME).unwrap().clone()
|
||||
}
|
||||
|
||||
fn unchecked_contract_code_id<D: TestableNymContract>(&self) -> u64 {
|
||||
*self.code_ids.get(D::NAME).unwrap()
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> ContractTester<C>
|
||||
@@ -247,10 +229,6 @@ where
|
||||
self.insert_common_storage_key(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unchecked_contract_address<D: TestableNymContract>(&self) -> Addr {
|
||||
self.well_known_contracts.get(D::NAME).unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Storage for ContractTester<C>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::{from_json, Binary, CustomQuery, QuerierWrapper, StdResult};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
// re-expose methods from QuerierWrapper as traits so that we could more easily define extension traits
|
||||
pub trait ContractQuerier {
|
||||
fn query_contract<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
msg: &impl Serialize,
|
||||
) -> StdResult<T>;
|
||||
|
||||
fn query_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<Vec<u8>>>;
|
||||
|
||||
fn query_contract_storage_value<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<T>> {
|
||||
match self.query_contract_storage(address, key)? {
|
||||
None => Ok(None),
|
||||
Some(value) => Ok(Some(from_json(&value)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> ContractQuerier for QuerierWrapper<'_, C>
|
||||
where
|
||||
C: CustomQuery,
|
||||
{
|
||||
fn query_contract<T: DeserializeOwned>(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
msg: &impl Serialize,
|
||||
) -> StdResult<T> {
|
||||
self.query_wasm_smart(address, msg)
|
||||
}
|
||||
|
||||
fn query_contract_storage(
|
||||
&self,
|
||||
address: impl Into<String>,
|
||||
key: impl Into<Binary>,
|
||||
) -> StdResult<Option<Vec<u8>>> {
|
||||
self.query_wasm_raw(address, key)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ pub mod events;
|
||||
pub mod signing;
|
||||
pub mod types;
|
||||
|
||||
pub mod contract_querier;
|
||||
pub mod helpers;
|
||||
|
||||
pub use types::*;
|
||||
|
||||
@@ -8,6 +8,11 @@ use nym_crypto::asymmetric::x25519::PublicKey;
|
||||
use nym_ip_packet_requests::IpPair;
|
||||
use nym_sphinx::addressing::{NodeIdentity, Recipient};
|
||||
|
||||
pub const DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_private_entry_wireguard.pem";
|
||||
pub const DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_public_entry_wireguard.pem";
|
||||
pub const DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_private_exit_wireguard.pem";
|
||||
pub const DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_public_exit_wireguard.pem";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct NymNode {
|
||||
pub identity: NodeIdentity,
|
||||
|
||||
Generated
-5
@@ -633,7 +633,6 @@ dependencies = [
|
||||
"cw4-group",
|
||||
"easy-addr",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-group-contract-common",
|
||||
"nym-multisig-contract-common",
|
||||
]
|
||||
@@ -664,7 +663,6 @@ dependencies = [
|
||||
"cw4",
|
||||
"easy-addr",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-group-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -1100,13 +1098,11 @@ dependencies = [
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"cw3-flex-multisig",
|
||||
"cw4",
|
||||
"cw4-group",
|
||||
"easy-addr",
|
||||
"nym-coconut-dkg-common",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-group-contract-common",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
@@ -1146,7 +1142,6 @@ dependencies = [
|
||||
"cosmwasm-std",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"nym-contracts-common",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"serde",
|
||||
|
||||
@@ -18,8 +18,6 @@ crate-type = ["cdylib", "rlib"]
|
||||
[dependencies]
|
||||
nym-coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
|
||||
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract", optional = true }
|
||||
|
||||
cosmwasm-schema = { workspace = true, optional = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
@@ -30,19 +28,13 @@ cw2 = { workspace = true }
|
||||
cw4 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
cw3-flex-multisig = { path = "../multisig/cw3-flex-multisig", features = ["testable-cw3-contract"], optional = true }
|
||||
cw4-group = { path = "../multisig/cw4-group", features = ["testable-cw4-contract"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" }
|
||||
cw-multi-test = { workspace = true }
|
||||
cw4-group = { path = "../multisig/cw4-group" }
|
||||
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" }
|
||||
|
||||
[features]
|
||||
schema-gen = ["nym-coconut-dkg-common/schema", "cosmwasm-schema"]
|
||||
testable-dkg-contract = ["nym-contracts-common-testing", "cw3-flex-multisig/testable-cw3-contract", "nym-group-contract-common", "cw4-group/testable-cw4-contract"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -280,50 +280,6 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Transfers ownership of the epoch dealer to another address. This assumes off-chain hand-over of keys",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"transfer_ownership"
|
||||
],
|
||||
"properties": {
|
||||
"transfer_ownership": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"transfer_to"
|
||||
],
|
||||
"properties": {
|
||||
"transfer_to": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Update announce address of this signer",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"update_announce_address"
|
||||
],
|
||||
"properties": {
|
||||
"update_announce_address": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"new_address"
|
||||
],
|
||||
"properties": {
|
||||
"new_address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
|
||||
@@ -191,50 +191,6 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Transfers ownership of the epoch dealer to another address. This assumes off-chain hand-over of keys",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"transfer_ownership"
|
||||
],
|
||||
"properties": {
|
||||
"transfer_ownership": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"transfer_to"
|
||||
],
|
||||
"properties": {
|
||||
"transfer_to": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Update announce address of this signer",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"update_announce_address"
|
||||
],
|
||||
"properties": {
|
||||
"update_announce_address": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"new_address"
|
||||
],
|
||||
"properties": {
|
||||
"new_address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
],
|
||||
"definitions": {
|
||||
|
||||
@@ -6,9 +6,7 @@ use crate::dealers::queries::{
|
||||
query_epoch_dealers_addresses_paged, query_epoch_dealers_paged,
|
||||
query_registered_dealer_details,
|
||||
};
|
||||
use crate::dealers::transactions::{
|
||||
try_add_dealer, try_transfer_ownership, try_update_announce_address,
|
||||
};
|
||||
use crate::dealers::transactions::try_add_dealer;
|
||||
use crate::dealings::queries::{
|
||||
query_dealer_dealings_status, query_dealing_chunk, query_dealing_chunk_status,
|
||||
query_dealing_metadata, query_dealing_status,
|
||||
@@ -23,6 +21,7 @@ use crate::epoch_state::transactions::{
|
||||
try_advance_epoch_state, try_initiate_dkg, try_trigger_reset, try_trigger_resharing,
|
||||
};
|
||||
use crate::error::ContractError;
|
||||
use crate::queued_migrations::introduce_historical_epochs;
|
||||
use crate::state::queries::query_state;
|
||||
use crate::state::storage::{DKG_ADMIN, MULTISIG, STATE};
|
||||
use crate::verification_key_shares::queries::{query_vk_share, query_vk_shares_paged};
|
||||
@@ -128,12 +127,6 @@ pub fn execute(
|
||||
ExecuteMsg::AdvanceEpochState {} => try_advance_epoch_state(deps, env),
|
||||
ExecuteMsg::TriggerReset {} => try_trigger_reset(deps, env, info),
|
||||
ExecuteMsg::TriggerResharing {} => try_trigger_resharing(deps, env, info),
|
||||
ExecuteMsg::TransferOwnership { transfer_to } => {
|
||||
try_transfer_ownership(deps, env, info, transfer_to)
|
||||
}
|
||||
ExecuteMsg::UpdateAnnounceAddress { new_address } => {
|
||||
try_update_announce_address(deps, info, new_address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,10 +248,12 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
|
||||
pub fn migrate(deps: DepsMut<'_>, env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
|
||||
set_build_information!(deps.storage)?;
|
||||
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
introduce_historical_epochs(deps, env)?;
|
||||
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::error::ContractError;
|
||||
use crate::Dealer;
|
||||
use cosmwasm_std::{StdResult, Storage};
|
||||
use cw_storage_plus::{Item, Map};
|
||||
use nym_coconut_dkg_common::dealer::{BlockHeight, OwnershipTransfer, TransactionIndex};
|
||||
use nym_coconut_dkg_common::types::{DealerDetails, DealerRegistrationDetails, EpochId, NodeIndex};
|
||||
|
||||
pub(crate) const DEALER_INDICES_PAGE_MAX_LIMIT: u32 = 80;
|
||||
@@ -24,9 +23,6 @@ pub(crate) const DEALERS_INDICES: Map<Dealer, NodeIndex> = Map::new("dealer_inde
|
||||
pub(crate) const EPOCH_DEALERS_MAP: Map<(EpochId, Dealer), DealerRegistrationDetails> =
|
||||
Map::new("epoch_dealers");
|
||||
|
||||
pub const OWNERSHIP_TRANSFER_LOG: Map<(Dealer, BlockHeight, TransactionIndex), OwnershipTransfer> =
|
||||
Map::new("transfer_log");
|
||||
|
||||
/// Attempts to retrieve a pre-assign node index associated with given dealer.
|
||||
/// If one doesn't exist, a new one is assigned.
|
||||
pub(crate) fn get_or_assign_index(
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::dealers::storage::{
|
||||
ensure_dealer, get_or_assign_index, is_dealer, save_dealer_details_if_not_a_dealer,
|
||||
DEALERS_INDICES, EPOCH_DEALERS_MAP, OWNERSHIP_TRANSFER_LOG,
|
||||
get_or_assign_index, is_dealer, save_dealer_details_if_not_a_dealer,
|
||||
};
|
||||
use crate::epoch_state::storage::{load_current_epoch, save_epoch};
|
||||
use crate::epoch_state::utils::check_epoch_state;
|
||||
use crate::error::ContractError;
|
||||
use crate::state::storage::STATE;
|
||||
use crate::Dealer;
|
||||
use cosmwasm_std::{Deps, DepsMut, Env, Event, MessageInfo, Response};
|
||||
use nym_coconut_dkg_common::dealer::{DealerRegistrationDetails, OwnershipTransfer};
|
||||
use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, Response};
|
||||
use nym_coconut_dkg_common::dealer::DealerRegistrationDetails;
|
||||
use nym_coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState};
|
||||
|
||||
fn ensure_group_member(deps: Deps, dealer: Dealer) -> Result<(), ContractError> {
|
||||
@@ -58,8 +57,7 @@ pub fn try_add_dealer(
|
||||
)?;
|
||||
|
||||
// check if it's a resharing dealer
|
||||
// SAFETY: resharing isn't allowed on 0th epoch
|
||||
#[allow(clippy::expect_used)]
|
||||
|
||||
let is_resharing_dealer = resharing
|
||||
&& is_dealer(
|
||||
deps.storage,
|
||||
@@ -85,90 +83,6 @@ pub fn try_add_dealer(
|
||||
Ok(Response::new().add_attribute("node_index", node_index.to_string()))
|
||||
}
|
||||
|
||||
pub fn try_transfer_ownership(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
transfer_to: String,
|
||||
) -> Result<Response, ContractError> {
|
||||
let transfer_to = deps.api.addr_validate(&transfer_to)?;
|
||||
|
||||
let epoch = load_current_epoch(deps.storage)?;
|
||||
|
||||
// make sure we're not mid-exchange
|
||||
check_epoch_state(deps.storage, EpochState::InProgress)?;
|
||||
|
||||
// make sure the requester is actually a dealer for this epoch
|
||||
ensure_dealer(deps.storage, &info.sender, epoch.epoch_id)?;
|
||||
|
||||
// make sure the new target dealer actually belong to the group
|
||||
ensure_group_member(deps.as_ref(), &transfer_to)?;
|
||||
|
||||
// update the index information
|
||||
let current_index = DEALERS_INDICES.load(deps.storage, &info.sender)?;
|
||||
DEALERS_INDICES.save(deps.storage, &transfer_to, ¤t_index)?;
|
||||
DEALERS_INDICES.remove(deps.storage, &info.sender);
|
||||
|
||||
// update registration detail for every epoch the current dealer has participated in the protocol
|
||||
// ideally, we'd have only updated the current epoch, but the way the contract is constructed
|
||||
// forbids that otherwise we'd have introduced inconsistency
|
||||
for epoch_id in 0..=epoch.epoch_id {
|
||||
if let Some(details) = EPOCH_DEALERS_MAP.may_load(deps.storage, (epoch_id, &info.sender))? {
|
||||
EPOCH_DEALERS_MAP.remove(deps.storage, (epoch_id, &info.sender));
|
||||
EPOCH_DEALERS_MAP.save(deps.storage, (epoch_id, &transfer_to), &details)?;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(transaction_info) = env.transaction else {
|
||||
return Err(ContractError::ExecutedOutsideTransaction);
|
||||
};
|
||||
|
||||
// save information about the transfer for more convenient history rebuilding
|
||||
OWNERSHIP_TRANSFER_LOG.save(
|
||||
deps.storage,
|
||||
(&info.sender, env.block.height, transaction_info.index),
|
||||
&OwnershipTransfer {
|
||||
node_index: current_index,
|
||||
from: info.sender.clone(),
|
||||
to: transfer_to.clone(),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(Response::new().add_event(
|
||||
Event::new("dkg-ownership-transfer")
|
||||
.add_attribute("from", info.sender)
|
||||
.add_attribute("to", transfer_to)
|
||||
.add_attribute("node_index", current_index.to_string()),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn try_update_announce_address(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
new_address: String,
|
||||
) -> Result<Response, ContractError> {
|
||||
let epoch = load_current_epoch(deps.storage)?;
|
||||
|
||||
// make sure we're not mid-exchange
|
||||
check_epoch_state(deps.storage, EpochState::InProgress)?;
|
||||
|
||||
// make sure the requester is actually a dealer for this epoch
|
||||
ensure_dealer(deps.storage, &info.sender, epoch.epoch_id)?;
|
||||
|
||||
let mut details = EPOCH_DEALERS_MAP.load(deps.storage, (epoch.epoch_id, &info.sender))?;
|
||||
let old_address = details.announce_address;
|
||||
|
||||
details.announce_address = new_address.clone();
|
||||
EPOCH_DEALERS_MAP.save(deps.storage, (epoch.epoch_id, &info.sender), &details)?;
|
||||
|
||||
Ok(Response::new().add_event(
|
||||
Event::new("dkg-announce-address-update")
|
||||
.add_attribute("dealer", info.sender)
|
||||
.add_attribute("old_address", old_address)
|
||||
.add_attribute("new_address", new_address),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
@@ -223,222 +137,3 @@ pub(crate) mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "testable-dkg-contract")]
|
||||
mod tests_with_mock {
|
||||
use super::*;
|
||||
use crate::testable_dkg_contract::{init_contract_tester, DkgContractTesterExt};
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use nym_contracts_common_testing::ContractOpts;
|
||||
|
||||
#[test]
|
||||
fn transferring_ownership() -> anyhow::Result<()> {
|
||||
let mut contract = init_contract_tester();
|
||||
let group_member = contract.random_group_member();
|
||||
|
||||
// sanity check, pre-dkg
|
||||
assert!(DEALERS_INDICES
|
||||
.may_load(&contract, &group_member)?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (0, &group_member))?
|
||||
.is_none());
|
||||
|
||||
contract.run_initial_dummy_dkg();
|
||||
let old_index = DEALERS_INDICES.load(&contract, &group_member)?;
|
||||
let old_details = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
|
||||
|
||||
let not_group_member = contract.addr_make("not_group_member");
|
||||
let (deps, env) = contract.deps_mut_env();
|
||||
assert!(try_transfer_ownership(
|
||||
deps,
|
||||
env,
|
||||
message_info(&group_member, &[]),
|
||||
not_group_member.to_string()
|
||||
)
|
||||
.is_err());
|
||||
|
||||
let new_group_member = contract.addr_make("new_group_member");
|
||||
contract.add_group_member(new_group_member.clone());
|
||||
let (deps, env) = contract.deps_mut_env();
|
||||
assert!(try_transfer_ownership(
|
||||
deps,
|
||||
env.clone(),
|
||||
message_info(&group_member, &[]),
|
||||
new_group_member.to_string()
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
// data under old key doesn't exist anymore
|
||||
assert!(DEALERS_INDICES
|
||||
.may_load(&contract, &group_member)?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (0, &group_member))?
|
||||
.is_none());
|
||||
|
||||
let new_index = DEALERS_INDICES.load(&contract, &new_group_member)?;
|
||||
let new_details = EPOCH_DEALERS_MAP.load(&contract, (0, &new_group_member))?;
|
||||
|
||||
// the underlying info hasn't changed
|
||||
assert_eq!(old_index, new_index);
|
||||
assert_eq!(old_details, new_details);
|
||||
|
||||
assert_eq!(
|
||||
OWNERSHIP_TRANSFER_LOG.load(
|
||||
&contract,
|
||||
(
|
||||
&group_member,
|
||||
env.block.height,
|
||||
env.transaction.unwrap().index
|
||||
)
|
||||
)?,
|
||||
OwnershipTransfer {
|
||||
node_index: new_index,
|
||||
from: group_member,
|
||||
to: new_group_member,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transferring_ownership_in_next_epochs() -> anyhow::Result<()> {
|
||||
let mut contract = init_contract_tester();
|
||||
let group_member = contract.random_group_member();
|
||||
|
||||
contract.run_initial_dummy_dkg(); // => epoch 0
|
||||
contract.run_reset_dkg(); // => epoch 1
|
||||
|
||||
// LEAVE DKG MEMBERSHIP
|
||||
contract.remove_group_member(group_member.clone());
|
||||
contract.run_reset_dkg(); // => epoch 2
|
||||
|
||||
// COME BACK
|
||||
contract.add_group_member(group_member.clone());
|
||||
contract.run_reset_dkg(); // => epoch 3
|
||||
|
||||
let old_index = DEALERS_INDICES.load(&contract, &group_member)?;
|
||||
let old_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
|
||||
let old_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &group_member))?;
|
||||
let old_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &group_member))?;
|
||||
assert!(old_details2.is_none());
|
||||
|
||||
let old_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &group_member))?;
|
||||
|
||||
// sanity check because we haven't changed our registration details:
|
||||
assert_eq!(old_details0, old_details1);
|
||||
assert_eq!(old_details1, old_details3);
|
||||
|
||||
let new_group_member = contract.addr_make("new_group_member");
|
||||
contract.add_group_member(new_group_member.clone());
|
||||
let (deps, env) = contract.deps_mut_env();
|
||||
assert!(try_transfer_ownership(
|
||||
deps,
|
||||
env.clone(),
|
||||
message_info(&group_member, &[]),
|
||||
new_group_member.to_string()
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
// data under old key doesn't exist anymore
|
||||
assert!(DEALERS_INDICES
|
||||
.may_load(&contract, &group_member)?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (0, &group_member))?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (1, &group_member))?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (2, &group_member))?
|
||||
.is_none());
|
||||
assert!(EPOCH_DEALERS_MAP
|
||||
.may_load(&contract, (3, &group_member))?
|
||||
.is_none());
|
||||
|
||||
let new_index = DEALERS_INDICES.load(&contract, &new_group_member)?;
|
||||
let new_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &new_group_member))?;
|
||||
let new_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &new_group_member))?;
|
||||
let new_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &new_group_member))?;
|
||||
let new_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &new_group_member))?;
|
||||
|
||||
// the underlying info hasn't changed
|
||||
assert_eq!(old_index, new_index);
|
||||
assert_eq!(old_details0, new_details0);
|
||||
assert_eq!(old_details1, new_details1);
|
||||
assert_eq!(old_details2, new_details2);
|
||||
assert_eq!(old_details3, new_details3);
|
||||
|
||||
assert_eq!(
|
||||
OWNERSHIP_TRANSFER_LOG.load(
|
||||
&contract,
|
||||
(
|
||||
&group_member,
|
||||
env.block.height,
|
||||
env.transaction.unwrap().index
|
||||
)
|
||||
)?,
|
||||
OwnershipTransfer {
|
||||
node_index: new_index,
|
||||
from: group_member,
|
||||
to: new_group_member,
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updating_announce_address() -> anyhow::Result<()> {
|
||||
let mut contract = init_contract_tester();
|
||||
let group_member = contract.random_group_member();
|
||||
|
||||
contract.run_initial_dummy_dkg(); // => epoch 0
|
||||
contract.run_reset_dkg(); // => epoch 1
|
||||
|
||||
// LEAVE DKG MEMBERSHIP
|
||||
contract.remove_group_member(group_member.clone());
|
||||
contract.run_reset_dkg(); // => epoch 2
|
||||
|
||||
// COME BACK
|
||||
contract.add_group_member(group_member.clone());
|
||||
contract.run_reset_dkg(); // => epoch 3
|
||||
|
||||
let old_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
|
||||
let old_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &group_member))?;
|
||||
let old_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &group_member))?;
|
||||
assert!(old_details2.is_none());
|
||||
let old_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &group_member))?;
|
||||
|
||||
// sanity check because we haven't changed our registration details:
|
||||
assert_eq!(old_details0, old_details1);
|
||||
assert_eq!(old_details1, old_details3);
|
||||
|
||||
let new_address = "https://new-address.com".to_string();
|
||||
try_update_announce_address(
|
||||
contract.deps_mut(),
|
||||
message_info(&group_member, &[]),
|
||||
new_address.clone(),
|
||||
)?;
|
||||
|
||||
let new_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
|
||||
let new_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &group_member))?;
|
||||
let new_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &group_member))?;
|
||||
assert!(new_details2.is_none());
|
||||
let new_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &group_member))?;
|
||||
|
||||
// old epoch data is unchanged
|
||||
assert_eq!(old_details0, new_details0);
|
||||
assert_eq!(old_details1, new_details1);
|
||||
assert_eq!(old_details2, new_details2);
|
||||
|
||||
// most recent entry is updated
|
||||
assert_eq!(new_details3.announce_address, new_address);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,4 @@ pub enum ContractError {
|
||||
|
||||
#[error("retrieved the maximum allowed number of cw4 members. for more the contracts have to be refactored")]
|
||||
PossiblyIncompleteGroupMembersQuery,
|
||||
|
||||
#[error("this method has been called outside transaction context")]
|
||||
ExecutedOutsideTransaction,
|
||||
}
|
||||
|
||||
@@ -15,6 +15,3 @@ mod queued_migrations;
|
||||
mod state;
|
||||
mod support;
|
||||
mod verification_key_shares;
|
||||
|
||||
#[cfg(feature = "testable-dkg-contract")]
|
||||
pub mod testable_dkg_contract;
|
||||
|
||||
@@ -1,2 +1,21 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::epoch_state::storage::HISTORICAL_EPOCH;
|
||||
use crate::error::ContractError;
|
||||
use cosmwasm_std::{DepsMut, Env};
|
||||
|
||||
pub fn introduce_historical_epochs(deps: DepsMut, env: Env) -> Result<(), ContractError> {
|
||||
if HISTORICAL_EPOCH.may_load(deps.storage)?.is_some() {
|
||||
return Err(ContractError::FailedMigration {
|
||||
comment: "this migration has already been run before".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
let current = crate::epoch_state::storage::CURRENT_EPOCH.load(deps.storage)?;
|
||||
// we won't have information on intermediate states prior to now, but that's not the end of the world
|
||||
HISTORICAL_EPOCH.save(deps.storage, ¤t, env.block.height)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::fixtures::TEST_MIX_DENOM;
|
||||
use crate::contract::instantiate;
|
||||
use crate::dealers::storage::{DEALERS_INDICES, EPOCH_DEALERS_MAP};
|
||||
use crate::epoch_state::storage::load_current_epoch;
|
||||
@@ -18,6 +17,8 @@ use nym_coconut_dkg_common::msg::InstantiateMsg;
|
||||
use nym_coconut_dkg_common::types::{DealerDetails, EpochId};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::fixtures::TEST_MIX_DENOM;
|
||||
|
||||
pub const ADMIN_ADDRESS: &str = addr!("admin address");
|
||||
pub const GROUP_CONTRACT: &str = addr!("group contract address");
|
||||
pub const MULTISIG_CONTRACT: &str = addr!("multisig contract address");
|
||||
@@ -73,7 +74,6 @@ pub fn add_fixture_dealer(deps: DepsMut<'_>) {
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
fn querier_handler(query: &WasmQuery) -> QuerierResult {
|
||||
let bin = match query {
|
||||
WasmQuery::Smart { contract_addr, msg } => {
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::ContractError;
|
||||
use cosmwasm_std::{Addr, QuerierWrapper};
|
||||
use cw4::Cw4Contract;
|
||||
|
||||
pub(crate) fn group_members(
|
||||
querier_wrapper: &QuerierWrapper,
|
||||
contract: &Cw4Contract,
|
||||
) -> Result<Vec<Addr>, ContractError> {
|
||||
// we shouldn't ever have more group members than the default limit but IN CASE
|
||||
// something changes down the line, do go through the pagination flow
|
||||
let mut group_members = Vec::new();
|
||||
|
||||
// current max limit
|
||||
let limit = 30;
|
||||
let mut start_after = None;
|
||||
loop {
|
||||
let members = contract.list_members(querier_wrapper, start_after, Some(limit))?;
|
||||
start_after = members.last().as_ref().map(|d| d.addr.clone());
|
||||
for member in &members {
|
||||
group_members.push(Addr::unchecked(&member.addr));
|
||||
}
|
||||
|
||||
if members.len() < limit as usize {
|
||||
// we have already exhausted the data
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(group_members)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::testable_dkg_contract::helpers::group_members;
|
||||
use crate::testable_dkg_contract::init_contract_tester_with_group_members;
|
||||
use cw4::Cw4Contract;
|
||||
use cw4_group::testable_cw4_contract::GroupContract;
|
||||
use nym_contracts_common_testing::ContractOpts;
|
||||
|
||||
#[test]
|
||||
fn getting_group_members() -> anyhow::Result<()> {
|
||||
for members in [0, 10, 100, 1000] {
|
||||
let tester = init_contract_tester_with_group_members(members);
|
||||
let group_contract =
|
||||
Cw4Contract::new(tester.unchecked_contract_address::<GroupContract>());
|
||||
let querier = tester.deps().querier;
|
||||
|
||||
let addresses = group_members(&querier, &group_contract)?;
|
||||
assert_eq!(addresses.len(), members);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,404 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// fine in test code
|
||||
#![allow(clippy::unwrap_used)]
|
||||
#![allow(clippy::expect_used)]
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use crate::error::ContractError;
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::Addr;
|
||||
use cw4::{Cw4Contract, Member};
|
||||
use nym_contracts_common_testing::{
|
||||
AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt, ChainOpts,
|
||||
CommonStorageKeys, ContractFn, ContractOpts, ContractTester, ContractTesterBuilder, DenomExt,
|
||||
PermissionedFn, QueryFn, RandExt, SliceRandom, TEST_DENOM,
|
||||
};
|
||||
|
||||
use crate::epoch_state::storage::load_current_epoch;
|
||||
use crate::state::storage::{MULTISIG, STATE};
|
||||
use crate::testable_dkg_contract::helpers::group_members;
|
||||
use nym_coconut_dkg_common::dealing::{DealingChunkInfo, PartialContractDealing};
|
||||
use nym_coconut_dkg_common::types::{Epoch, EpochState};
|
||||
use nym_contracts_common::dealings::ContractSafeBytes;
|
||||
|
||||
pub use cw3_flex_multisig::testable_cw3_contract::{Duration, MultisigContract, Threshold};
|
||||
pub use cw4_group::testable_cw4_contract::GroupContract;
|
||||
pub use nym_coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use nym_contracts_common_testing::TestableNymContract;
|
||||
|
||||
pub(crate) mod helpers;
|
||||
|
||||
pub struct DkgContract;
|
||||
|
||||
const DEFAULT_GROUP_MEMBERS: usize = 15;
|
||||
|
||||
impl TestableNymContract for DkgContract {
|
||||
const NAME: &'static str = "dkg-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = ContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
query
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn init() -> ContractTester<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
init_contract_tester_with_group_members(DEFAULT_GROUP_MEMBERS)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_contract_tester() -> ContractTester<DkgContract> {
|
||||
DkgContract::init().with_common_storage_key(CommonStorageKeys::Admin, "dkg-admin")
|
||||
}
|
||||
|
||||
pub fn prepare_contract_tester_builder_with_group_members<C>(
|
||||
members: usize,
|
||||
) -> ContractTesterBuilder<C>
|
||||
where
|
||||
C: TestableNymContract,
|
||||
{
|
||||
let mut builder = ContractTesterBuilder::<C>::new();
|
||||
let api = builder.api();
|
||||
|
||||
// 1. init the CW4 group contract
|
||||
let group_init_msg = cw4_group::testable_cw4_contract::InstantiateMsg {
|
||||
admin: Some(builder.master_address().to_string()),
|
||||
members: (0..members)
|
||||
.map(|i| Member {
|
||||
addr: api.addr_make(&format!("group-member-{i}")).to_string(),
|
||||
weight: 1,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
builder.instantiate_contract::<GroupContract>(Some(group_init_msg));
|
||||
|
||||
// we just instantiated it
|
||||
let group_contract_address = builder.unchecked_contract_address::<GroupContract>();
|
||||
|
||||
// 2. init the CW3 multisig contract WITH DUMMY VALUES
|
||||
let multisig_init_msg = cw3_flex_multisig::testable_cw3_contract::InstantiateMsg {
|
||||
group_addr: group_contract_address.to_string(),
|
||||
// \/ PLACEHOLDERS
|
||||
coconut_bandwidth_contract_address: group_contract_address.to_string(),
|
||||
coconut_dkg_contract_address: group_contract_address.to_string(),
|
||||
// /\ PLACEHOLDERS
|
||||
threshold: Threshold::AbsolutePercentage {
|
||||
percentage: "0.67".parse().unwrap(),
|
||||
},
|
||||
max_voting_period: Duration::Time(3600),
|
||||
executor: None,
|
||||
proposal_deposit: None,
|
||||
};
|
||||
builder.instantiate_contract::<MultisigContract>(Some(multisig_init_msg));
|
||||
|
||||
// we just instantiated it
|
||||
let multisig_contract_address = builder.unchecked_contract_address::<MultisigContract>();
|
||||
|
||||
// 3. init the DKG contract
|
||||
let dkg_init_msg = InstantiateMsg {
|
||||
group_addr: group_contract_address.to_string(),
|
||||
multisig_addr: multisig_contract_address.to_string(),
|
||||
time_configuration: None,
|
||||
mix_denom: TEST_DENOM.to_string(),
|
||||
key_size: 5,
|
||||
};
|
||||
builder.instantiate_contract::<DkgContract>(Some(dkg_init_msg));
|
||||
|
||||
// we just instantiated it
|
||||
let dkg_contract_address = builder.unchecked_contract_address::<DkgContract>();
|
||||
|
||||
// 4. migrate the multisig contract to hold correct addresses
|
||||
let multisig_migrate_msg = cw3_flex_multisig::testable_cw3_contract::MigrateMsg {
|
||||
// \/ STILL A PLACEHOLDER (this contract does not care about interactions with the ecash contract)
|
||||
coconut_bandwidth_address: dkg_contract_address.to_string(),
|
||||
// /\ STILL A PLACEHOLDER
|
||||
coconut_dkg_address: dkg_contract_address.to_string(),
|
||||
};
|
||||
builder.migrate_contract::<MultisigContract>(&multisig_migrate_msg);
|
||||
builder
|
||||
}
|
||||
|
||||
pub fn init_contract_tester_with_group_members(members: usize) -> ContractTester<DkgContract> {
|
||||
prepare_contract_tester_builder_with_group_members(members)
|
||||
.build()
|
||||
.with_common_storage_key(CommonStorageKeys::Admin, "dkg-admin")
|
||||
}
|
||||
|
||||
pub trait DkgContractTesterExt:
|
||||
ContractOpts<ExecuteMsg = ExecuteMsg, QueryMsg = QueryMsg, ContractError = ContractError>
|
||||
+ ChainOpts
|
||||
+ AdminExt
|
||||
+ DenomExt
|
||||
+ RandExt
|
||||
+ BankExt
|
||||
+ ArbitraryContractStorageReader
|
||||
+ ArbitraryContractStorageWriter
|
||||
{
|
||||
fn epoch(&self) -> Epoch {
|
||||
load_current_epoch(self.storage()).unwrap()
|
||||
}
|
||||
|
||||
fn multisig_contract(&self) -> Addr {
|
||||
MULTISIG.get(self.deps()).unwrap().unwrap()
|
||||
}
|
||||
|
||||
fn group_contract_wrapper(&self) -> Cw4Contract {
|
||||
STATE.load(self.storage()).unwrap().group_addr
|
||||
}
|
||||
|
||||
fn remove_group_member(&mut self, addr: Addr) {
|
||||
// we have the same admin for all contracts
|
||||
let admin = self.admin().unwrap();
|
||||
|
||||
self.execute_arbitrary_contract(
|
||||
self.unchecked_contract_address::<GroupContract>(),
|
||||
message_info(&admin, &[]),
|
||||
&nym_group_contract_common::msg::ExecuteMsg::UpdateMembers {
|
||||
remove: vec![addr.to_string()],
|
||||
add: vec![],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn add_group_member(&mut self, addr: Addr) {
|
||||
let querier = self.deps().querier;
|
||||
|
||||
let members = self
|
||||
.group_contract_wrapper()
|
||||
.list_members(&querier, None, None)
|
||||
.unwrap();
|
||||
let weight = members.first().map(|m| m.weight).unwrap_or(1);
|
||||
|
||||
// we have the same admin for all contracts
|
||||
let admin = self.admin().unwrap();
|
||||
|
||||
self.execute_arbitrary_contract(
|
||||
self.unchecked_contract_address::<GroupContract>(),
|
||||
message_info(&admin, &[]),
|
||||
&nym_group_contract_common::msg::ExecuteMsg::UpdateMembers {
|
||||
remove: vec![],
|
||||
add: vec![Member {
|
||||
addr: addr.to_string(),
|
||||
weight,
|
||||
}],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn group_members(&self) -> Vec<Addr> {
|
||||
let querier = self.deps().querier;
|
||||
let group_contract = self.group_contract_wrapper();
|
||||
group_members(&querier, &group_contract).unwrap()
|
||||
}
|
||||
|
||||
fn random_group_member(&mut self) -> Addr {
|
||||
let members = self.group_members();
|
||||
members
|
||||
.choose(&mut self.raw_rng())
|
||||
.expect("no group members available")
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn dummy_dkg_steps(&mut self, resharing: bool) {
|
||||
let admin = self.admin().unwrap();
|
||||
let group_members = self.group_members();
|
||||
|
||||
// 2. register dealers
|
||||
for group_member in &group_members {
|
||||
self.execute_msg(
|
||||
group_member.clone(),
|
||||
&ExecuteMsg::RegisterDealer {
|
||||
bte_key_with_proof: format!("btekey-{group_member}"),
|
||||
identity_key: format!("identity-{group_member}"),
|
||||
announce_address: format!("announce-address-{group_member}"),
|
||||
resharing,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// PublicKeySubmission => DealingExchange
|
||||
self.advance_time_by(600);
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::DealingExchange { resharing }
|
||||
);
|
||||
|
||||
// 3. exchange dealings
|
||||
for group_member in &group_members {
|
||||
self.execute_msg(
|
||||
group_member.clone(),
|
||||
&ExecuteMsg::CommitDealingsMetadata {
|
||||
dealing_index: 1,
|
||||
chunks: vec![DealingChunkInfo { size: 1 }],
|
||||
resharing,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
self.execute_msg(
|
||||
group_member.clone(),
|
||||
&ExecuteMsg::CommitDealingsChunk {
|
||||
chunk: PartialContractDealing {
|
||||
dealing_index: 1,
|
||||
chunk_index: 0,
|
||||
data: ContractSafeBytes(vec![0]),
|
||||
},
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// DealingExchange => VerificationKeySubmission
|
||||
self.advance_time_by(300);
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::VerificationKeySubmission { resharing }
|
||||
);
|
||||
|
||||
// 4. derive keypairs
|
||||
for group_member in &group_members {
|
||||
self.execute_msg(
|
||||
group_member.clone(),
|
||||
&ExecuteMsg::CommitVerificationKeyShare {
|
||||
share: format!("partial-vk-{group_member}"),
|
||||
resharing,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// VerificationKeySubmission => VerificationKeyValidation
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
|
||||
.unwrap();
|
||||
self.advance_time_by(60);
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::VerificationKeyValidation { resharing }
|
||||
);
|
||||
|
||||
// VerificationKeyValidation => VerificationKeyFinalization
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::VerificationKeyFinalization { resharing }
|
||||
);
|
||||
|
||||
// 5. validate keys
|
||||
for group_member in &group_members {
|
||||
self.execute_msg(
|
||||
self.multisig_contract(),
|
||||
&ExecuteMsg::VerifyVerificationKeyShare {
|
||||
owner: group_member.to_string(),
|
||||
resharing,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// VerificationKeyFinalization => InProgress
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
|
||||
.unwrap();
|
||||
assert_eq!(self.epoch().state, EpochState::InProgress)
|
||||
}
|
||||
|
||||
fn run_initial_dummy_dkg(&mut self) {
|
||||
assert_eq!(self.epoch().state, EpochState::WaitingInitialisation);
|
||||
// 1. initiate DKG
|
||||
// WaitingInitialisation => PublicKeySubmission
|
||||
let admin = self.admin().unwrap();
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::InitiateDkg {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
|
||||
self.dummy_dkg_steps(false)
|
||||
}
|
||||
|
||||
fn run_reset_dkg(&mut self) {
|
||||
// 1. reset DKG
|
||||
// InProgress => PublicKeySubmission
|
||||
let admin = self.admin().unwrap();
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::TriggerReset {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
self.dummy_dkg_steps(false)
|
||||
}
|
||||
|
||||
fn run_resharing_dkg(&mut self) {
|
||||
assert_eq!(self.epoch().state, EpochState::InProgress);
|
||||
|
||||
let group_members = self.group_members();
|
||||
println!(
|
||||
"epoch: {} members: {}",
|
||||
self.epoch().epoch_id,
|
||||
group_members.len()
|
||||
);
|
||||
|
||||
// 1. initiate DKG
|
||||
// InProgress => PublicKeySubmission
|
||||
let admin = self.admin().unwrap();
|
||||
self.execute_msg(admin.clone(), &ExecuteMsg::TriggerResharing {})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
self.epoch().state,
|
||||
EpochState::PublicKeySubmission { resharing: true }
|
||||
);
|
||||
self.dummy_dkg_steps(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl DkgContractTesterExt for ContractTester<DkgContract> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dealers::storage::EPOCH_DEALERS_MAP;
|
||||
|
||||
#[test]
|
||||
fn dummy_resharing() {
|
||||
let mut contract = init_contract_tester_with_group_members(10);
|
||||
contract.run_initial_dummy_dkg();
|
||||
|
||||
let dealer = contract.random_group_member();
|
||||
let details = EPOCH_DEALERS_MAP
|
||||
.may_load(contract.storage(), (0, &dealer))
|
||||
.unwrap();
|
||||
assert!(details.is_some());
|
||||
|
||||
assert_eq!(contract.epoch().epoch_id, 0);
|
||||
|
||||
contract.run_resharing_dkg();
|
||||
assert_eq!(contract.epoch().epoch_id, 1);
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ pub mod test_helpers {
|
||||
use cosmwasm_std::{Env, Response, Timestamp, Uint128};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::events::{
|
||||
MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY,
|
||||
may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY,
|
||||
};
|
||||
use mixnet_contract_common::helpers::compare_decimals;
|
||||
use mixnet_contract_common::mixnode::{NodeRewarding, UnbondedMixnode};
|
||||
@@ -100,8 +100,8 @@ pub mod test_helpers {
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) use nym_contracts_common_testing::helpers::{find_attribute, FindAttribute};
|
||||
use std::fmt::Debug;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub(crate) fn sorted_addresses(n: usize) -> Vec<Addr> {
|
||||
let mut rng = test_rng();
|
||||
@@ -1592,6 +1592,83 @@ pub mod test_helpers {
|
||||
None
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn find_attribute<S: Into<String>>(
|
||||
event_type: Option<S>,
|
||||
attribute: &str,
|
||||
response: &Response,
|
||||
) -> String {
|
||||
let event_type = event_type.map(Into::into);
|
||||
for event in &response.events {
|
||||
if let Some(typ) = &event_type {
|
||||
if &event.ty != typ {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(attr) = may_find_attribute(event, attribute) {
|
||||
return attr;
|
||||
}
|
||||
}
|
||||
// this is only used in tests so panic here is fine
|
||||
panic!("did not find the attribute")
|
||||
}
|
||||
|
||||
pub(crate) trait FindAttribute {
|
||||
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>;
|
||||
|
||||
fn any_attribute(&self, attribute: &str) -> String {
|
||||
self.attribute::<_, String>(None, attribute)
|
||||
}
|
||||
|
||||
fn any_parsed_attribute<T>(&self, attribute: &str) -> T
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Debug,
|
||||
{
|
||||
self.parsed_attribute::<_, String, T>(None, attribute)
|
||||
}
|
||||
|
||||
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Debug;
|
||||
|
||||
fn decimal<E, S>(&self, event_type: E, attribute: &str) -> Decimal
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
{
|
||||
self.parsed_attribute(event_type, attribute)
|
||||
}
|
||||
}
|
||||
|
||||
impl FindAttribute for Response {
|
||||
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
{
|
||||
find_attribute(event_type.into(), attribute, self)
|
||||
}
|
||||
|
||||
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
|
||||
where
|
||||
E: Into<Option<S>>,
|
||||
S: Into<String>,
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: Debug,
|
||||
{
|
||||
find_attribute(event_type.into(), attribute, self)
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
// using floats in tests is fine
|
||||
// (what it does is converting % value, like 12.34 into `Performance` (`Percent`)
|
||||
// which internally is represented by decimal `0.1234`
|
||||
|
||||
@@ -16,6 +16,10 @@ required-features = ["cosmwasm-schema"]
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
# use library feature to disable all instantiate/execute/query exports
|
||||
library = []
|
||||
|
||||
[dependencies]
|
||||
cw-utils = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
@@ -30,15 +34,9 @@ cosmwasm-std = { workspace = true }
|
||||
nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-multisig-contract-common = { path = "../../../common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-contracts-common-testing = { path = "../../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
easy-addr = { path = "../../../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
cw4-group = { path = "../cw4-group" }
|
||||
cw-multi-test = { workspace = true }
|
||||
cw20-base = { workspace = true }
|
||||
|
||||
[features]
|
||||
# use library feature to disable all instantiate/execute/query exports
|
||||
library = []
|
||||
testable-cw3-contract = ["nym-contracts-common-testing"]
|
||||
@@ -23,6 +23,3 @@ For more information on this contract, please check out the
|
||||
*/
|
||||
|
||||
pub mod contract;
|
||||
|
||||
#[cfg(feature = "testable-cw3-contract")]
|
||||
pub mod testable_cw3_contract;
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use nym_contracts_common_testing::{ContractFn, PermissionedFn, QueryFn};
|
||||
use nym_multisig_contract_common::error::ContractError;
|
||||
|
||||
pub use cw_utils::{Duration, Threshold};
|
||||
pub use nym_contracts_common_testing::TestableNymContract;
|
||||
pub use nym_multisig_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
|
||||
pub struct MultisigContract;
|
||||
|
||||
impl TestableNymContract for MultisigContract {
|
||||
const NAME: &'static str = "cw3-flex-multisig-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = ContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
|deps, env, msg| query(deps, env, msg).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,14 @@ name = "schema"
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
# use library feature to disable all instantiate/execute/query exports
|
||||
library = []
|
||||
|
||||
|
||||
[dependencies]
|
||||
nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-contracts-common-testing = { path = "../../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
|
||||
|
||||
cw-utils = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
@@ -34,14 +38,9 @@ cw-controllers = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
||||
schemars = "0.8.1"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
easy-addr = { path = "../../../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
|
||||
[features]
|
||||
# use library feature to disable all instantiate/execute/query exports
|
||||
library = []
|
||||
testable-cw4-contract = ["nym-contracts-common-testing"]
|
||||
easy-addr = { path = "../../../common/cosmwasm-smart-contracts/easy_addr" }
|
||||
@@ -23,6 +23,3 @@ pub use crate::error::ContractError;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "testable-cw4-contract")]
|
||||
pub mod testable_cw4_contract;
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use crate::error::ContractError;
|
||||
use nym_contracts_common_testing::{ContractFn, PermissionedFn, QueryFn};
|
||||
|
||||
pub use nym_contracts_common_testing::TestableNymContract;
|
||||
pub use nym_group_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
|
||||
pub struct GroupContract;
|
||||
|
||||
impl TestableNymContract for GroupContract {
|
||||
const NAME: &'static str = "cw4-group-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = ContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
|deps, env, msg| query(deps, env, msg).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
Open the needed ports for `nym-node` by running these commands:
|
||||
|
||||
```sh
|
||||
ufw allow 22/tcp # SSH - you're in control of these ports
|
||||
ufw allow 80/tcp # HTTP
|
||||
ufw allow 443/tcp # HTTPS
|
||||
ufw allow 1789/tcp # Nym specific - Mixnet
|
||||
ufw allow 1790/tcp # Nym specific - Verloc
|
||||
ufw allow 8080/tcp # Nym specific - nym-node-api
|
||||
ufw allow 9000/tcp # Nym Specific - clients port
|
||||
ufw allow 9001/tcp # Nym specific - wss port
|
||||
ufw allow 51822/udp # WireGuard
|
||||
ufw allow in on nymwg to any port 51830 proto tcp # bandwidth queries/topup - inside the tunnel
|
||||
ufw allow 22/tcp # SSH - you're in control of these ports
|
||||
ufw allow 80/tcp # HTTP
|
||||
ufw allow 443/tcp # HTTPS
|
||||
ufw allow 1789/tcp # Nym specific
|
||||
ufw allow 1790/tcp # Nym specific
|
||||
ufw allow 8080/tcp # Nym specific - nym-node-api
|
||||
ufw allow 9000/tcp # Nym Specific - clients port
|
||||
ufw allow 9001/tcp # Nym specific - wss port
|
||||
ufw allow 51822/udp # WireGuard
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"mixmining_reserve": {
|
||||
"denom": "unym",
|
||||
"amount": "180875972213757"
|
||||
"amount": "182883243257647"
|
||||
},
|
||||
"vesting_tokens": {
|
||||
"denom": "unym",
|
||||
@@ -13,6 +13,6 @@
|
||||
},
|
||||
"circulating_supply": {
|
||||
"denom": "unym",
|
||||
"amount": "819124027786243"
|
||||
"amount": "817116756742353"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
819_124_027
|
||||
817_116_756
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
5_024
|
||||
5_080
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
250_614
|
||||
250_000
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
60_147_392
|
||||
60_000_000
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
60_147_391
|
||||
60_000_000
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
| **Item** | **Description** | **Amount in NYM** |
|
||||
|:-------------------|:------------------------------------------------------|--------------------:|
|
||||
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 180_875_972 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 182_883_243 |
|
||||
| Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 819_124_027 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 250_614 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 817_116_756 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 250_000 |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"interval": {
|
||||
"reward_pool": "180875972213757.135840914936141948",
|
||||
"staking_supply": "60147391744900.170789956043539529",
|
||||
"reward_pool": "182883243257647.891553460395608456",
|
||||
"staking_supply": "60000000000000",
|
||||
"staking_supply_scale_factor": "0.07342892",
|
||||
"epoch_reward_budget": "5024332561.493253773358748226",
|
||||
"stake_saturation_point": "250614132270.417378291483514748",
|
||||
"epoch_reward_budget": "5080090090.490219209818344322",
|
||||
"stake_saturation_point": "250000000000",
|
||||
"sybil_resistance": "0.3",
|
||||
"active_set_work_factor": "10",
|
||||
"interval_pool_emission": "0.02"
|
||||
|
||||
@@ -1 +1 @@
|
||||
Wednesday, October 1st 2025, 11:16:15 UTC
|
||||
Tuesday, September 16th 2025, 11:07:26 UTC
|
||||
|
||||
@@ -58,8 +58,8 @@ Options:
|
||||
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
|
||||
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
|
||||
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT>
|
||||
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-announced-port <WIREGUARD_ANNOUNCED_PORT>
|
||||
Port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
|
||||
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
|
||||
--verloc-bind-address <VERLOC_BIND_ADDRESS>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"sandbox": "Sandbox Testnet",
|
||||
"binaries": "Binaries",
|
||||
"nodes": "Nodes & Validators Guides",
|
||||
"tools": "Tools",
|
||||
"troubleshooting": "Troubleshooting",
|
||||
"tokenomics": "Tokenomics",
|
||||
"faq": "FAQ",
|
||||
|
||||
@@ -48,174 +48,6 @@ This page displays a full list of all the changes during our release cycle from
|
||||
|
||||
<VarInfo />
|
||||
|
||||
## `v2025.17-isabirra`
|
||||
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.17-isabirra)
|
||||
- [`nym-node`](nodes/nym-node.mdx) version `1.18.0`
|
||||
|
||||
```sh
|
||||
nym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2025-10-01T10:42:58.647419869Z
|
||||
Build Version: 1.18.0
|
||||
Commit SHA: bbea2ff9e913f49cb7bf6c7bafa9d9b158c80de5
|
||||
Commit Date: 2025-10-01T12:06:07.000000000+02:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.88.0
|
||||
rustc Channel: stable
|
||||
cargo Profile: release
|
||||
```
|
||||
|
||||
### Operators Updates & Tools
|
||||
|
||||
<Callout type="info" emoji="ℹ️">
|
||||
**With `nym-node` version `1.18.0` operators need to make `tcp/51830` reachable on the WG interface (for bandwidth queries/topup).**
|
||||
|
||||
This is inside the tunnel - no need to expose it publicly unless you want an external test.
|
||||
**Run this command to expose it:**
|
||||
```
|
||||
ufw allow in on nymwg to any port 51830 proto tcp
|
||||
```
|
||||
</Callout>
|
||||
|
||||
With this platform upgrade we are releasing several tools to improve operator experience. Check them out and let us know what you think.
|
||||
|
||||
- **New program for `nym-node` automated installation**: [`nym-node-cli.py`](tools#nym-node-cli)
|
||||
|
||||
- **New node reward tracker**: [`node_rewards_tracker.py`](tools#cmd-reward-tracker)
|
||||
|
||||
- **New server ping testing diagnostic tool**: [`test-nodes-pings.sh`](tools#node-ping-tester)
|
||||
|
||||
- **New [Tools page](tools)**
|
||||
|
||||
- **New Gateway landing page [template](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/landing-page.html)** and simplified [documentation for reverse proxy configuration](nodes/nym-node/configuration/proxy-configuration#html-file-customization)
|
||||
|
||||
- SpectreDAO Explorer has a new [Delegation Wizzard](https://explorer.nym.spectredao.net/delegate): Allowing for a smart multi-delegation with optimal node selection form Keplr Wallet
|
||||
|
||||
### API Changes
|
||||
|
||||
Please read carefully below to follow up with API removal and changes to ensure that your development is up to date.
|
||||
|
||||
#### Nym API Removed Routes
|
||||
|
||||
All of the routes removed had already been deprecated over a year ago. This is mostly due to the fact that either they were returning only data on legacy nodes (that could never be used anyway) or required some non-sense conversions. Open the dropdown below to see removed routes
|
||||
<br/>
|
||||
<AccordionTemplate name="Removed API routes">
|
||||
|
||||
### Legacy mixnodes related:
|
||||
|
||||
- `/v1/mixnodes`
|
||||
- `/v1/mixnodes/active`
|
||||
- `/v1/mixnodes/active/detailed`
|
||||
- `/v1/mixnodes/described`
|
||||
- `/v1/mixnodes/rewarded`
|
||||
- `/v1/mixnodes/rewarded/detailed`
|
||||
- `/v1/mixnodes/detailed`
|
||||
- `/v1/mixnodes/blacklisted`
|
||||
- `/v1/status/mixnodes/active/detailed`
|
||||
- `/v1/status/mixnodes/rewarded/detailed`
|
||||
- `/v1/status/mixnodes/detailed`
|
||||
- `/v1/status/mixnodes/inclusion-probability`
|
||||
- `/v1/status/mixnodes/inclusion-probability`
|
||||
- `/v1/status/mixnode/{mix_id}/inclusion-probability`
|
||||
- `/v1/status/mixnode/{mix_id}/stake-saturation`
|
||||
- `/v1/status/mixnode/{mix_id}/status`
|
||||
- `/v1/status/mixnode/{mix_id}/reward-estimation`
|
||||
- `/v1/status/mixnode/{mix_id}/compute-reward-estimation`
|
||||
- `/v1/status/mixnodes/detailed-unfiltered`
|
||||
- `/v1/status/mixnode/{mix_id}/report`
|
||||
- `/v1/status/mixnode/{mix_id}/avg_uptime`
|
||||
|
||||
### Legacy gateways related:
|
||||
|
||||
- `/v1/gateways`
|
||||
- `/v1/gateways/described`
|
||||
- `/v1/gateways/blacklisted`
|
||||
- `/v1/status/gateways/detailed`
|
||||
- `/v1/status/gateways/detailed-unfiltered`
|
||||
- `/v1/status/gateway/{identity}/report`
|
||||
- `/v1/status/gateway/{identity}/avg_uptime`
|
||||
|
||||
</AccordionTemplate>
|
||||
|
||||
#### Structs changes:
|
||||
|
||||
- `MixnodeUptimeHistoryResponse` no longer has `owner` field
|
||||
- `GatewayUptimeHistoryResponse` no longer has `owner` field
|
||||
|
||||
|
||||
#### New Routes Added
|
||||
|
||||
- `/v1/nym-nodes/stake-saturation/{node_id}` - as a better replacement for `/v1/status/mixnode/{mix_id}/stake-saturation` as this information might be potentially useful and can be applied to any nym-node, not just a legacy mixnode.
|
||||
- `/v1/legacy/mixnodes` - returns a list of bonded legacy mixnodes that haven't migrated to nym-nodes
|
||||
- `/v1/legacy/gateways` - returns a list of bonded legacy gateways that haven't migrated to nym-nodes
|
||||
|
||||
#### Node Status API
|
||||
|
||||
Furthermore the changes remove all scraping of legacy mixnodes from NS and the following routes are removed:
|
||||
|
||||
- `/v2/mixnodes/{mix_id}`
|
||||
- `/v2/mixnodes`
|
||||
|
||||
### Features
|
||||
|
||||
- [Refresh mixnet contract on epoch progression](https://github.com/nymtech/nym/pull/6023): Currently when an epoch has advanced, `nym-api` might still be serving data from the previous iteration. This PR makes sure the data is refreshed as soon as possible so new role assignment would be available quickly after.
|
||||
|
||||
- [Explorer-v2: Replace recommended servers with automated selection](https://github.com/nymtech/nym/pull/6019): Use params to get 10 nodes, no more manual paste.
|
||||
|
||||
- [Credential proxy crate](https://github.com/nymtech/nym/pull/6018): This PR moves 90% of the credential proxy functionalities into a common crate instead.
|
||||
|
||||
- [Moving clients crate from vpn-client repo to here](https://github.com/nymtech/nym/pull/6015): As part of the registration-client work, moved `nym-authenticator-client`, `nym-ip-packet-client` and `nym-wg-gateway-client` from the vpn-client repo into this repository.
|
||||
|
||||
- [Cancellation migration](https://github.com/nymtech/nym/pull/6014): Migrates shutdown watchers from `TaskManager` into the new `ShutdownManager`, which relies on tokio’s `CancellationToken` and `TaskTracker`.
|
||||
|
||||
- [Use `ShutdownToken` for nym-api](https://github.com/nymtech/nym/pull/5997): Makes `nym-api` use `ShutdownToken` (tokio `CancellationToken` inside) for cancellation; groundwork for migrating clients to the same approach.
|
||||
|
||||
- [Delegation program stake checker and adjuster](https://github.com/nymtech/nym/pull/5980): Adds a script to generate a CSV for delegation adjustments according to Delegation Program rules, including node/stake/uptime/version and other fields.
|
||||
|
||||
- [Domain fronting integration](https://github.com/nymtech/nym/pull/5974): Completes migration to `nym_http_api_client::Client`, enabling domain fronting and unifying HTTP client usage across the monorepo.
|
||||
|
||||
- [Shared library for attempting to retrieve update mode attestation](https://github.com/nymtech/nym/pull/5954): Part of NET-341, provides a shared library to attempt retrieving update mode attestation.
|
||||
|
||||
- [Credential proxy deposit pool](https://github.com/nymtech/nym/pull/5945): Changes the credential proxy to hold a pool of available deposits and monitor quorum state, reducing wastage and improving reliability.
|
||||
|
||||
- [Nym signers monitor](https://github.com/nymtech/nym/pull/5933): Independent tool for checking the status of network signers and sending notifications if “upgrade” mode is detected.
|
||||
|
||||
- [Nym node autorun CLI](https://github.com/nymtech/nym/pull/5916): Adds an interactive CLI to install, set up, and configure `nym-node` as a systemd service, improving operator experience.
|
||||
|
||||
### Bugfix
|
||||
|
||||
- [Fix the registration handshake](https://github.com/nymtech/nym/pull/6062): Resolves issues in the registration handshake process.
|
||||
|
||||
- [Return from MixTrafficController if client request channel has closed](https://github.com/nymtech/nym/pull/6002): Ensures the MixTrafficController exits properly when the client request channel closes.
|
||||
|
||||
- [Use default value for the ports until api is deployed](https://github.com/nymtech/nym/pull/6007): Fixes VPN querying mainnet for the API and not finding some of the newly added values by using default port values until the API is deployed.
|
||||
|
||||
- [Recipient deserialisation for deserialisers missing bytes specialisation](https://github.com/nymtech/nym/pull/5991): Fixes deserialization where formats like TOML/JSON defaulted incorrectly, ignoring bytes-related optimisations.
|
||||
|
||||
- [Use WASM compatible time API in client](https://github.com/nymtech/nym/pull/5948): Prevents client crashes in WASM by replacing time APIs unavailable in that environment.
|
||||
|
||||
### Refactors & Maintenance
|
||||
|
||||
- [Convenience for ShutdownTracker](https://github.com/nymtech/nym/pull/6038): Adds a method to create a `ShutdownToken` from a `CancellationToken`. Exposes `ShutdownTracker` in the SDK.
|
||||
|
||||
- [Made http-api-client-macro doctest compile](https://github.com/nymtech/nym/pull/6037): Adjusts doctests in `http-api-client-macro` so they compile.
|
||||
|
||||
- [Remove legacy nodes from nym api](https://github.com/nymtech/nym/pull/6021): Removes nearly all references to legacy nodes from `nym-api` and node status API; cleans up deprecated endpoints and adjusts structs.
|
||||
|
||||
- [Upgraded syn to 2.0 and removed nym-execute](https://github.com/nymtech/nym/pull/5998): Updates the `syn` dependency and removes `nym-execute`.
|
||||
|
||||
- [Use updated version of simulate endpoint](https://github.com/nymtech/nym/pull/5988): Switches to the updated simulate endpoint.
|
||||
|
||||
- [Purge temp databases on build](https://github.com/nymtech/nym/pull/5984): Prevents build failures when switching to branches without DB migrations by cleaning temp databases during builds.
|
||||
|
||||
- [Internal hidden command to force advance nyx epoch](https://github.com/nymtech/nym/pull/5964): Adds a hidden developer command to advance the Nyx epoch manually.
|
||||
|
||||
- [Create an axum_test client for more integrated unit testing](https://github.com/nymtech/nym/pull/5956): Adds an `axum_test` client to support more integrated unit tests.
|
||||
|
||||
- [Revert "Create an axum_test client for more integrated unit testing"](https://github.com/nymtech/nym/pull/5999): Reverts PR #5956 due to OpenSSL dependency issues.
|
||||
|
||||
|
||||
## `v2025.16-halloumi`
|
||||
|
||||
- **[`nym-node`](nodes/nym-node.mdx) is not part of the release, the latest stays on version `1.16.0`**
|
||||
|
||||
+254
-10
@@ -29,13 +29,14 @@ The commands in this setup need to be run with root permission. Either add a pre
|
||||
|
||||
## Reverse Proxy Setup
|
||||
|
||||
Operators running nodes facing open internet may benefit from having a landing page. This page serves as a source of useful information about Nym network and the node when they try to search node IP or hostname.
|
||||
The following snippet needs be modified as described below according to the public identity that you may want to show on this public notice, i.e. your graphics and your email.
|
||||
It would allow you to serve it as a landing page resembling the one proposed by [Tor](https://gitlab.torproject.org/tpo/core/tor/-/raw/HEAD/contrib/operator-tools/tor-exit-notice.html) but with all the changes needed to adhere to the Nym's operators case.
|
||||
|
||||
|
||||
|
||||
### HTML File Customization
|
||||
|
||||
File for html configuration are by convention located at `/var/www/<HOSTNAME>` directory and it's sub-directories. We refer to this directory as `<LANDING_PAGE_ASSETS_PATH>`.
|
||||
File for html configuration are by convention located at `/var/www/<HOSTNAME>` directory and it's subdirectories. We refer to this directory as `<LANDING_PAGE_ASSETS_PATH>`.
|
||||
|
||||
<Steps>
|
||||
|
||||
@@ -46,21 +47,264 @@ mkdir -p /var/www/<HOSTNAME>
|
||||
|
||||
###### 2. Create html landing page
|
||||
|
||||
- Use your own html code (check this [markdown to html tool](https://markdowntohtml.com/)) or use [this template](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/landing-page.html)
|
||||
- Use your own html code (check this [markdown to html tool](https://markdowntohtml.com/)) or copy the template below to a new file called `index.html` located in `/var/www/<HOSTNAME>` directory.
|
||||
|
||||
- Copy the [template](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/landing-page.html) a new file called `index.html` located in `/var/www/<HOSTNAME>` directory.
|
||||
|
||||
###### 3. If you used the template above - before you save and close the file, make sure to edit the email address:
|
||||
<AccordionTemplate name={<IndexPage/>}>
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>This is a NYM Exit Gateway</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="">
|
||||
<style>
|
||||
:root {
|
||||
font-family: Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace;
|
||||
}
|
||||
:root{
|
||||
--background-color: #121726;
|
||||
--text-color: #f2f2f2;
|
||||
--link-color: #fb6e4e;
|
||||
}
|
||||
html{
|
||||
background: var(--background-color);
|
||||
}
|
||||
body{
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 5vw;
|
||||
padding-right: 5vw;
|
||||
max-width: 1000px;
|
||||
}
|
||||
h1{
|
||||
font-size: 55px;
|
||||
text-align: center;
|
||||
color: var(--title-color)
|
||||
}
|
||||
p{
|
||||
color: var(--text-color);
|
||||
}
|
||||
p, a{
|
||||
font-size: 20px;
|
||||
}
|
||||
a{
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover{
|
||||
filter: brightness(.8);
|
||||
text-decoration: underline;
|
||||
}
|
||||
.links{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.links > a{
|
||||
margin: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
- Change the email address you're willing to use for being contacted.
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>This is a NYM Exit Gateway</h1>
|
||||
<p style="text-align:center">
|
||||
<img class="logo" src="<FIXME>">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You are most likely accessing this website because you've had some issue with
|
||||
the traffic coming from this IP. This router is part of the <a
|
||||
href="https://nym.com/">NYM project</a>, which is
|
||||
dedicated to <a href="https://nym.com/about/mission">create</a> outstanding
|
||||
privacy software that is legally compliant without sacrificing integrity or
|
||||
having any backdoors.
|
||||
This router IP should be generating no other traffic, unless it has been
|
||||
compromised.</p>
|
||||
|
||||
<p>
|
||||
The Nym mixnet is operated by a decentralised community of node operators
|
||||
and stakers. The Nym mixnet is trustless, meaning that no parts of the system
|
||||
nor its operators have access to information that might compromise the privacy
|
||||
of users. Nym software enacts a strict principle of data minimisation and has
|
||||
no back doors. The Nym mixnet works by encrypting packets in several layers
|
||||
and relaying those through a multi-layered network called a mixnet, eventually
|
||||
letting the traffic exit the Nym mixnet through an exit gateway like this one.
|
||||
This design makes it very hard for a service to know which user is connecting to it,
|
||||
since it can only see the IP-address of the Nym exit gateway:</p>
|
||||
|
||||
<p style="text-align:center;margin:40px 0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="500" viewBox="0 0 490.28 293.73" style="width:100%;max-width:600px">
|
||||
<desc>Illustration showing how a user might connect to a service through the Nym network. The user first sends their data through three daisy-chained encrypted Nym nodes that exist on three different continents. Then the last Nym node in the chain connects to the target service over the normal internet.</desc>
|
||||
<defs>
|
||||
<style>
|
||||
.t{
|
||||
fill: var(--text-color);
|
||||
stroke: var(--text-color);
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path fill="#6fc8b7" d="M257.89 69.4c-6.61-6.36-10.62-7.73-18.36-8.62-7.97-1.83-20.06-7.99-24.17-.67-3.29 5.85-18.2 12.3-16.87 2.08.92-7.03 11.06-13.28 17-17.37 8.69-5.99 24.97-2.87 26.1-10.28 1.04-6.86-8.33-13.22-8.55-2.3-.38 12.84-19.62 2.24-8.73-6.2 8.92-6.9 16.05-9.02 25.61-6.15 12.37 4.83 25.58-2.05 33.73-.71 12.37-2.01 24.69-5.25 37.39-3.96 13 .43 24.08-.14 37.06.63 9.8 1.58 16.5 2.87 26.37 3.6 6.6.48 17.68-.82 24.3 1.9 8.3 4.24.44 10.94-6.89 11.8-8.79 1.05-23.59-1.19-26.6 1.86-5.8 7.41 10.75 5.68 11.27 14.54.57 9.45-5.42 9.38-8.72 16-2.7 4.2.3 13.93-1.18 18.45-1.85 5.64-19.64 4.47-14.7 14.4 4.16 8.34 1.17 19.14-10.33 12.02-5.88-3.65-9.85-22.04-15.66-21.9-11.06.27-11.37 13.18-12.7 17.52-1.3 4.27-3.79 2.33-6-.63-3.54-4.76-7.75-14.22-12.01-17.32-6.12-4.46-10.75-1.17-15.55 2.83-5.63 4.69-8.78 7.82-7.46 16.5.78 9.1-12.9 15.84-14.98 24.09-2.61 10.32-2.57 22.12-8.81 31.47-4 5.98-14.03 20.12-21.27 14.97-7.5-5.34-7.22-14.6-9.56-23.08-2.5-9.02.6-17.35-2.57-26.2-2.45-6.82-6.23-14.54-13.01-13.24-6.5.92-15.08 1.38-19.23-2.97-5.65-5.93-6-10.1-6.61-18.56 1.65-6.94 5.79-12.64 10.38-18.63 3.4-4.42 17.45-10.39 25.26-7.83 10.35 3.38 17.43 10.5 28.95 8.57 3.12-.53 9.14-4.65 7.1-6.62zm-145.6 37.27c-4.96-1.27-11.57 1.13-11.8 6.94-1.48 5.59-4.82 10.62-5.8 16.32.56 6.42 4.34 12.02 8.18 16.97 3.72 3.85 8.58 7.37 9.3 13.1 1.24 5.88 1.6 11.92 2.28 17.87.34 9.37.95 19.67 7.29 27.16 4.26 3.83 8.4-2.15 6.52-6.3-.54-4.54-.6-9.11 1.01-13.27 4.2-6.7 7.32-10.57 12.44-16.64 5.6-7.16 12.74-11.75 14-20.9.56-4.26 5.72-13.86 1.7-16.72-3.14-2.3-15.83-4-18.86-6.49-2.36-1.71-3.86-9.2-9.86-12.07-4.91-3.1-10.28-6.73-16.4-5.97zm11.16-49.42c6.13-2.93 10.58-4.77 14.61-10.25 3.5-4.28 2.46-12.62-2.59-15.45-7.27-3.22-13.08 5.78-18.81 8.71-5.96 4.2-12.07-5.48-6.44-10.6 5.53-4.13.38-9.2-5.66-8.48-6.12.8-12.48-1.45-18.6-1.73-5.3-.7-10.13-1-15.45-1.37-5.37-.05-16.51-2.23-25.13.87-5.42 1.79-12.5 5.3-16.73 9.06-4.85 4.2.2 7.56 5.54 7.45 5.3-.22 16.8-5.36 20.16.98 3.68 8.13-5.82 18.29-5.2 26.69.1 6.2 3.37 11 4.74 16.98 1.62 5.94 6.17 10.45 10 15.14 4.7 5.06 13.06 6.3 19.53 8.23 7.46.14 3.34-9.23 3.01-14.11 1.77-7.15 8.49-7.82 12.68-13.5 7.14-7.72 16.41-13.4 24.34-18.62zM190.88 3.1c-4.69 0-13.33.04-18.17-.34-7.65.12-13.1-.62-19.48-1.09-3.67.39-9.09 3.34-5.28 7.04 3.8.94 7.32 4.92 7.1 9.31 1.32 4.68 1.2 11.96 6.53 13.88 4.76-.2 7.12-7.6 11.93-8.25 6.85-2.05 12.5-4.58 17.87-9.09 2.48-2.76 7.94-6.38 5.26-10.33-1.55-1.31-2.18-.64-5.76-1.13zm178.81 157.37c-2.66 10.08-5.88 24.97 9.4 15.43 7.97-5.72 12.58-2.02 17.47 1.15.5.43 2.65 9.2 7.19 8.53 5.43-2.1 11.55-5.1 14.96-11.2 2.6-4.62 3.6-12.39 2.76-13.22-3.18-3.43-6.24-11.03-7.7-15.1-.76-2.14-2.24-2.6-2.74-.4-2.82 12.85-6.04 1.22-10.12-.05-8.2-1.67-29.62 7.17-31.22 14.86z"/>
|
||||
<g fill="none">
|
||||
<path stroke="#cf63a6" stroke-linecap="round" stroke-width="2.76" d="M135.2 140.58c61.4-3.82 115.95-118.83 151.45-103.33"/>
|
||||
<path stroke="#cf63a6" stroke-linecap="round" stroke-width="2.76" d="M74.43 46.66c38.15 8.21 64.05 42.26 60.78 93.92M286.65 37.25c-9.6 39.44-3.57 57.12-35.64 91.98"/>
|
||||
<path stroke="#e4c101" stroke-dasharray="9.06,2.265" stroke-width="2.27" d="M397.92 162.52c-31.38 1.26-90.89-53.54-148.3-36.17"/>
|
||||
<path stroke="#cf63a6" stroke-linecap="round" stroke-width="2.77" d="M17.6 245.88c14.35 0 14.4.05 28-.03"/>
|
||||
<path stroke="#e3bf01" stroke-dasharray="9.06,2.265" stroke-width="2.27" d="M46.26 274.14c-17.52-.12-16.68.08-30.34.07"/>
|
||||
</g>
|
||||
<g transform="translate(120.8 -35.81)">
|
||||
<circle cx="509.78" cy="68.74" r="18.12" fill="#240a3b" transform="translate(-93.3 38.03) scale(.50637)"/>
|
||||
<circle cx="440.95" cy="251.87" r="18.12" fill="#240a3b" transform="translate(-93.3 38.03) scale(.50637)"/>
|
||||
<circle cx="212.62" cy="272.19" r="18.12" fill="#240a3b" transform="translate(-93.3 38.03) scale(.50637)"/>
|
||||
<circle cx="92.12" cy="87.56" r="18.12" fill="#240a3b" transform="translate(-93.3 38.03) scale(.50637)"/>
|
||||
<circle cx="730.88" cy="315.83" r="18.12" fill="#67727b" transform="translate(-93.3 38.03) scale(.50637)"/>
|
||||
<circle cx="-102.85" cy="282.18" r="9.18" fill="#240a3b"/>
|
||||
<circle cx="-102.85" cy="309.94" r="9.18" fill="#67727b"/>
|
||||
</g>
|
||||
<g class="t">
|
||||
<text xml:space="preserve" x="-24.76" y="10.37" stroke-width=".26" font-size="16.93" font-weight="700" style="line-height:1.25" transform="translate(27.79 2.5)" word-spacing="0"><tspan x="-24.76" y="10.37">The user</tspan></text>
|
||||
<text xml:space="preserve" x="150.63" y="196.62" stroke-width=".26" font-size="16.93" font-weight="700" style="line-height:1.25" transform="translate(27.79 2.5)" word-spacing="0"><tspan x="150.63" y="196.62">This server</tspan></text>
|
||||
<text xml:space="preserve" x="346.39" y="202.63" stroke-width=".26" font-size="16.93" font-weight="700" style="line-height:1.25" transform="translate(27.79 2.5)" word-spacing="0"><tspan x="346.39" y="202.63">Your service</tspan></text>
|
||||
<text xml:space="preserve" x="34.52" y="249.07" stroke-width=".26" font-size="16.93" font-weight="700" style="line-height:1.25" transform="translate(27.79 2.5)" word-spacing="0"><tspan x="34.52" y="249.07">Nym network link</tspan></text>
|
||||
<text xml:space="preserve" x="34.13" y="276.05" stroke-width=".26" font-size="16.93" font-weight="700" style="line-height:1.25" transform="translate(27.79 2.5)" word-spacing="0"><tspan x="34.13" y="276.05">Unencrypted link</tspan></text>
|
||||
<path fill="none" stroke-linecap="round" stroke-width="1.67" d="M222.6 184.1c-2.6-15.27 8.95-23.6 18.43-38.86m186.75 45.61c-.68-10.17-9.4-17.68-18.08-23.49"/>
|
||||
<path fill="none" stroke-linecap="round" stroke-width="1.67" d="M240.99 153.41c.35-3.41 1.19-6.17.04-8.17m-7.15 5.48c1.83-2.8 4.58-4.45 7.15-5.48"/>
|
||||
<path fill="none" stroke-linecap="round" stroke-width="1.67" d="M412.43 173.21c-2.2-3.15-2.54-3.85-2.73-5.85m0 0c2.46-.65 3.85.01 6.67 1.24M61.62 40.8C48.89 36.98 36.45 27.54 36.9 18.96M61.62 40.8c.05-2.58-3.58-4.8-5.25-5.26m-2.65 6.04c1.8.54 6.8 1.31 7.9-.78"/>
|
||||
<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.44" d="M1.22 229.4h247.74v63.1H1.22z"/>
|
||||
</g>
|
||||
</svg>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://nym.com/about/mixnet">Read more about how Nym works.</a></p>
|
||||
|
||||
<p>
|
||||
Nym relies on a growing ecosystem of users, developers and researcher partners
|
||||
aligned with the mission to make sure Nym software is running, remains usable
|
||||
and solves real problems. While Nym is not designed for malicious computer
|
||||
users, it is true that they can use the network for malicious ends. This
|
||||
is largely because criminals and hackers have significantly better access to
|
||||
privacy and anonymity than do the regular users whom they prey upon. Criminals
|
||||
can and do build, sell, and trade far larger and more powerful networks than
|
||||
Nym on a daily basis. Thus, in the mind of this operator, the social need for
|
||||
easily accessible censorship-resistant private, anonymous communication trumps
|
||||
the risk of unskilled bad actors, who are almost always more easily uncovered
|
||||
by traditional police work than by extensive monitoring and surveillance anyway.</p>
|
||||
|
||||
<p>
|
||||
In terms of applicable law, the best way to understand Nym is to consider it a
|
||||
network of routers operating as common carriers, much like the Internet
|
||||
backbone. However, unlike the Internet backbone routers, Nym mixnodes do not
|
||||
contain identifiable routing information about the source of a packet and do
|
||||
mix the user internet traffic with that of other users, making communications
|
||||
private and protecting not just the user content but the metadata
|
||||
(user's IP address, who the user talks to, when, where, from what device and
|
||||
more) and no single Nym node can determine both the origin and destination
|
||||
of a given transmission.</p>
|
||||
|
||||
<p>
|
||||
As such, there is little the operator of this Exit Gateway can do to help you
|
||||
track the connection further. This Exit Gateway maintains no logs of any of the
|
||||
Nym mixnet traffic, so there is little that can be done to trace either legitimate or
|
||||
illegitimate traffic (or to filter one from the other). Attempts to
|
||||
seize this router will accomplish nothing.</p>
|
||||
|
||||
<!-- FIXME: US-Only section. Remove if you are a non-US operator -->
|
||||
<!--
|
||||
<p>
|
||||
Furthermore, this machine also serves as a carrier of email, which means that
|
||||
its contents are further protected under the ECPA. <a
|
||||
href="https://www.law.cornell.edu/uscode/text/18/2707">18
|
||||
USC 2707</a> explicitly allows for civil remedies ($1000/account
|
||||
<i>plus</i> legal fees)
|
||||
in the event of a seizure executed without good faith or probable cause (it
|
||||
should be clear at this point that traffic with an originating IP address of
|
||||
FIXME_DNS_NAME should not constitute probable cause to seize the
|
||||
machine). Similar considerations exist for 1st amendment content on this
|
||||
machine.</p>
|
||||
-->
|
||||
<!-- FIXME: May or may not be US-only. Some non-US tor nodes have in
|
||||
fact reported DMCA harassment... -->
|
||||
<!--
|
||||
<p>
|
||||
If you are a representative of a company who feels that this router is being
|
||||
used to violate the DMCA, please be aware that this machine does not host or
|
||||
contain any illegal content. Also be aware that network infrastructure
|
||||
maintainers are not liable for the type of content that passes over their
|
||||
equipment, in accordance with <a
|
||||
href="https://www.law.cornell.edu/uscode/text/17/512">DMCA
|
||||
"safe harbor" provisions</a>. In other words, you will have just as much luck
|
||||
sending a takedown notice to the Internet backbone providers.
|
||||
</p>
|
||||
-->
|
||||
|
||||
<p>To decentralise and enable privacy for a broad range of services, this
|
||||
Exit Gateway adopts an <a href="https://nymtech.net/.wellknown/network-requester/exit-policy.txt">Exit Policy</a>
|
||||
in accordance with the <a href="https://tornull.org/">Tor Null ‘deny’ list</a>
|
||||
and the <a href="https://tornull.org/tor-reduced-reduced-exit-policy.php">Tor reduced policy</a>,
|
||||
which are two established safeguards.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
That being said, if you still have a complaint about the router, you may email the
|
||||
<a href="mailto:>YOUR_EMAIL_ADDRESS>">maintainer</a>. If complaints are related
|
||||
to a particular service that is being abused, the maintainer will submit that to the
|
||||
NYM Operators Community in order to add it to the Exit Policy cited above.
|
||||
If approved, that would prevent this router from allowing that traffic to exit through it.
|
||||
That can be done only on an IP+destination port basis, however. Common P2P ports are already blocked.</p>
|
||||
|
||||
<p>
|
||||
You also have the option of blocking this IP address and others on the Nym network if you so desire.
|
||||
The Nym project provides a <a href="https://nym.com/explorer">
|
||||
web service</a> to fetch a list of all IP addresses of Nym Gateway Exit nodes that allow exiting to a
|
||||
specified IP:port combination. Please be considerate when using these options.</p>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
</AccordionTemplate>
|
||||
|
||||
###### 3. If you used the template above - before you save and close the file, make sure to edit the text, especially the information in these points:
|
||||
|
||||
- Add your own favicon logo on the line:
|
||||
```html
|
||||
<link rel="icon" type="image/png" href="">
|
||||
```
|
||||
|
||||
- Add your header logo on the line:
|
||||
```html
|
||||
<img class="logo" src="<FIXME>">
|
||||
```
|
||||
|
||||
- By either setting the URl to the image (if you're hosting it publicly, i.e. on your web server)
|
||||
```html
|
||||
href="<PATH_TO_YOUR_PUBLIC_URL>"
|
||||
|
||||
# and
|
||||
|
||||
src="<PATH_TO_YOUR_PUBLIC_URL>"
|
||||
```
|
||||
|
||||
- **or** by adding the image inline as base64 encoded image
|
||||
```html
|
||||
href="href="data:image/x-icon;base64,AAABAAMA....""
|
||||
|
||||
# and
|
||||
|
||||
src="href="data:image/x-icon;base64,AAABAAMA....""
|
||||
```
|
||||
|
||||
- Add the email address you're willing to use for being contacted.
|
||||
```
|
||||
<a href="mailto:><YOUR_EMAIL_ADDRESS>">maintainer</a>
|
||||
```
|
||||
|
||||
- Additionally you can add your own favicon logo on the line:
|
||||
```html
|
||||
<link rel="icon" type="YOUR_FAVICON_IMAGE_PATH" href="">
|
||||
```
|
||||
- If you're running the node within the US check the sections marked as `FIXME`, add your DNS name and un-comment those.
|
||||
|
||||
###### 4. Save and exit
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
|
||||
```sh
|
||||
nym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2025-10-01T10:42:58.647419869Z
|
||||
Build Version: 1.18.0
|
||||
Commit SHA: bbea2ff9e913f49cb7bf6c7bafa9d9b158c80de5
|
||||
Commit Date: 2025-10-01T12:06:07.000000000+02:00
|
||||
Build Timestamp: 2025-08-05T09:14:30.322593213Z
|
||||
Build Version: 1.16.0
|
||||
Commit SHA: 7f97f13799342f864e1b106e8cafc9f6d6c24c0f
|
||||
Commit Date: 2025-07-24T11:00:58.000000000+01:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.88.0
|
||||
rustc Version: 1.86.0
|
||||
rustc Channel: stable
|
||||
cargo Profile: release
|
||||
```
|
||||
|
||||
@@ -87,6 +87,11 @@ ufw status
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
- In case of reverse proxy setup add:
|
||||
```sh
|
||||
ufw allow 443/tcp
|
||||
```
|
||||
|
||||
- Re-check the status of the firewall:
|
||||
```sh
|
||||
ufw status
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
import { Callout } from 'nextra/components';
|
||||
import { Tabs } from 'nextra/components';
|
||||
import { MyTab } from 'components/generic-tabs.tsx';
|
||||
import { RunTabs } from 'components/operators/nodes/node-run-command-tabs';
|
||||
import { VarInfo } from 'components/variable-info.tsx';
|
||||
import { AccordionTemplate } from 'components/accordion-template.tsx';
|
||||
import { Steps } from 'nextra/components';
|
||||
|
||||
|
||||
# Tools
|
||||
|
||||
On this page you can find tools to [setup a node automatically](#nym-node-cli), [explorers](#explorers) and other useful dashboards and [scripts](#cmd-reward-tracker).
|
||||
|
||||
<VarInfo />
|
||||
|
||||
## Explorers
|
||||
|
||||
Nym Network stats can be humanly read on some of the explorers and dashboards.
|
||||
|
||||
- **[Nym Explorer v2](https://nym.com/explorer):** Official Nym Explorer
|
||||
|
||||
- **[SpectreDAO Explorer](https://explorer.nym.spectredao.net/):** By operators for operators - currently the most used Nym explorer
|
||||
|
||||
- **[Nymesis](https://nymesis.vercel.app/):** A slick dashboard by operator community
|
||||
|
||||
- **[Nym Harbourmaster](https://harbourmaster.nymtech.net/):** A dashboard showing results of Gateway probes and more (*in development*)
|
||||
|
||||
## Nym Node CLI
|
||||
|
||||
This interactive command-line-based tool takes an operator through a journey of installing, configuring and starting a `nym-node` as a systemd service, doing most of the steps automatically for them.
|
||||
|
||||
**Installation & Running**
|
||||
|
||||
<Steps>
|
||||
|
||||
###### 1. SSH into your server (VPS)
|
||||
|
||||
- The installation of `nym-node` using this program requires you to run as `root`
|
||||
|
||||
###### 2. Download `nym-node-cli.py` and make executable
|
||||
|
||||
```sh
|
||||
wget https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/nym-node-cli.py && \
|
||||
chmod +x ./nym-node-cli.py
|
||||
```
|
||||
|
||||
###### 3. Run the program
|
||||
```
|
||||
./nym-node-cli.py
|
||||
```
|
||||
###### 4. Read and follow the prompts
|
||||
</ Steps>
|
||||
|
||||
## CMD Reward Tracker
|
||||
|
||||
A command-line-based program locally calculating nodes rewards based on provided Nyx account addresses in `data/wallet-addresses.csv`.
|
||||
|
||||
**Installation & Running**
|
||||
|
||||
<Steps>
|
||||
|
||||
###### 1. Pull / clone `nymtech/nym` repository
|
||||
|
||||
- Open terminal and navigate to where you want to have `nym` repostiry and run:
|
||||
```sh
|
||||
git clone https://github.com/nymtech/nym
|
||||
```
|
||||
|
||||
###### 2. Add your Nyx accounts to `wallet-addresses.csv`
|
||||
|
||||
- Navigate to `nym/scripts/rewards-tracker/data`
|
||||
- Open `wallet-addresses.csv` in your favourite text editor or a sheet managing tool (Like Libre Office Calc)
|
||||
- To the first collumn called `address` add all Nyx addresses you want to track
|
||||
- Delete all `add_wallet_or_delete` template examples
|
||||
|
||||
###### 3. Add entity to `wallet-addresses.csv` - optional
|
||||
|
||||
- In the same file operators who want to separate their nodes by an entity, can add this entity to the `tag` column
|
||||
- If not leave this column empty - delete all `optional_tag_or_delete` fields
|
||||
|
||||
- Csv example with `tag`s:
|
||||
```
|
||||
address, tag
|
||||
n1foofoofoo, personal
|
||||
n1barbarbar, personal
|
||||
n1bazbazbaz, mysquad
|
||||
n1lollollol, mysquad
|
||||
```
|
||||
- For operators having all nodes under one entity, the tag field will be left empty. Example:
|
||||
```csv
|
||||
address, tag
|
||||
n1foofoofoo
|
||||
n1barbarbar
|
||||
n1bazbazbaz
|
||||
```
|
||||
|
||||
###### 4. Save `wallet-addresses.csv` and exit
|
||||
|
||||
###### 5. Run the program
|
||||
|
||||
- In terminal navigate to `nym/scripts/rewards-tracker`
|
||||
- Run the program:
|
||||
```
|
||||
./node_rewards_tracker.py
|
||||
```
|
||||
</ Steps>
|
||||
|
||||
**The Output**
|
||||
|
||||
The result of running `node_rewards_tracker.py` is:
|
||||
|
||||
1. Printed table in terminal
|
||||
2. Updated sheet with complete info stored in `data/node-balances.csv`
|
||||
3. Historical data file stored in `data/data.yaml` - this file should not be changed manually, as all values older than 30 days get auto-removed
|
||||
|
||||
|
||||
## Node Ping Tester
|
||||
|
||||
This tool is used to diagnose how many nodes providing self-described endpoint allow your IP to ping them. It's a very simple script fetching all [`/described`](https://validator.nymtech.net/api/v1/nym-nodes/described) nodes and trying to ping each of them.
|
||||
|
||||
The output is collected into two files:
|
||||
```
|
||||
├── ping_not_working.csv
|
||||
└── ping_works.csv
|
||||
```
|
||||
|
||||
**Installation & Running**
|
||||
|
||||
<Steps>
|
||||
|
||||
###### 1. SSH into your node server (VPS)
|
||||
|
||||
###### 2. Download and make executable
|
||||
|
||||
- Navigate to the directory where you want to have this script
|
||||
- Download and make executable:
|
||||
```sh
|
||||
wget https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/test-nodes-pings.sh && \
|
||||
chmod +x test-nodes-pings.sh
|
||||
```
|
||||
|
||||
###### 3. Run the script
|
||||
|
||||
- Default running command is straight forward
|
||||
```sh
|
||||
./test-nodes-pings.sh
|
||||
```
|
||||
- If you want to increase the `ping` attempts from default 2 or lower the concurency, feel free to change the variables, like this:
|
||||
```sh
|
||||
PING_RETRIES=10 PING_TIMEOUT=5 CONCURRENCY=16 ./test-nodes-pings.sh
|
||||
```
|
||||
</Steps>
|
||||
|
||||
You can look up the IPs from `ping_not_working.csv`, using some online database, like [ipinfo.io](https://ipinfo.io).
|
||||
|
||||
Feel invited to share the outcome with Nym team, mentors and the rest of the operators in our [Matrix Node Operators channel](https://matrix.to/#/#operators:nymtech.chat).
|
||||
@@ -67,8 +67,7 @@ pub struct PaginatedCachedNodesExpandedResponseSchema {
|
||||
/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded.
|
||||
#[utoipa::path(
|
||||
operation_id = "v2_nodes_expanded",
|
||||
tag = "Unstable Nym Nodes v2",
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParamsWithRole),
|
||||
path = "",
|
||||
|
||||
@@ -15,8 +15,7 @@ use nym_api_requests::nym_nodes::NodeRoleQueryParam;
|
||||
/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded.
|
||||
#[utoipa::path(
|
||||
operation_id = "v2_nodes_basic_all",
|
||||
tag = "Unstable Nym Nodes v2",
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParamsWithRole),
|
||||
path = "",
|
||||
@@ -53,8 +52,7 @@ pub(crate) async fn nodes_basic_all(
|
||||
/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and support mixing role.
|
||||
#[utoipa::path(
|
||||
operation_id = "v2_mixnodes_basic_all",
|
||||
tag = "Unstable Nym Nodes v2",
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/mixnodes/all",
|
||||
@@ -77,8 +75,7 @@ pub(crate) async fn mixnodes_basic_all(
|
||||
/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and are in the active set with one of the mixing roles.
|
||||
#[utoipa::path(
|
||||
operation_id = "v2_mixnodes_basic_active",
|
||||
tag = "Unstable Nym Nodes v2",
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/mixnodes/active",
|
||||
@@ -101,8 +98,7 @@ pub(crate) async fn mixnodes_basic_active(
|
||||
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and support entry gateway role.
|
||||
#[utoipa::path(
|
||||
operation_id = "v2_entry_gateways_basic_all",
|
||||
tag = "Unstable Nym Nodes v2",
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/entry-gateways",
|
||||
@@ -125,8 +121,7 @@ pub(crate) async fn entry_gateways_basic_all(
|
||||
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded and support exit gateway role.
|
||||
#[utoipa::path(
|
||||
operation_id = "v2_exit_gateways_basic_all",
|
||||
tag = "Unstable Nym Nodes v2",
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/exit-gateways",
|
||||
|
||||
@@ -14,6 +14,7 @@ workspace = true
|
||||
[dependencies]
|
||||
bincode.workspace = true
|
||||
futures.workspace = true
|
||||
rand.workspace = true
|
||||
semver.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio-util.workspace = true
|
||||
@@ -24,6 +25,7 @@ nym-authenticator-requests = { path = "../common/authenticator-requests" }
|
||||
nym-bandwidth-controller = { path = "../common/bandwidth-controller" }
|
||||
nym-credentials-interface = { path = "../common/credentials-interface" }
|
||||
nym-crypto = { path = "../common/crypto" }
|
||||
nym-pemstore = { path = "../common/pemstore" }
|
||||
nym-registration-common = { path = "../common/registration" }
|
||||
nym-sdk = { path = "../sdk/rust/nym-sdk" }
|
||||
nym-service-provider-requests-common = { path = "../common/service-provider-requests-common" }
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_crypto::asymmetric::x25519::KeyPair;
|
||||
use nym_pemstore::KeyPairPath;
|
||||
use nym_sdk::mixnet::{IncludedSurbs, Recipient, TransmissionLane};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
pub(crate) fn create_input_message(
|
||||
recipient: Recipient,
|
||||
@@ -24,3 +27,23 @@ pub(crate) fn create_input_message(
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_or_generate_keypair<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
paths: KeyPairPath,
|
||||
) -> KeyPair {
|
||||
match nym_pemstore::load_keypair(&paths) {
|
||||
Ok(keypair) => keypair,
|
||||
Err(_) => {
|
||||
let keypair = KeyPair::new(rng);
|
||||
if let Err(e) = nym_pemstore::store_keypair(&keypair, &paths) {
|
||||
tracing::error!(
|
||||
"could not store generated keypair at {:?} - {:?}; will use ephemeral keys",
|
||||
paths,
|
||||
e
|
||||
);
|
||||
}
|
||||
keypair
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::mixnet_listener::{MixnetMessageBroadcastReceiver, MixnetMessageInputSender};
|
||||
use crate::{helpers, ClientMessage, Error, Result};
|
||||
use nym_authenticator_requests::{
|
||||
client_message::QueryMessageImpl, response::AuthenticatorResponse, traits::Id, v2, v3, v4, v5,
|
||||
AuthenticatorVersion,
|
||||
};
|
||||
use nym_credentials_interface::CredentialSpendingData;
|
||||
use nym_crypto::asymmetric::x25519::{KeyPair, PublicKey};
|
||||
use nym_sdk::mixnet::{IncludedSurbs, Recipient};
|
||||
use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
|
||||
impl crate::AuthenticatorClient {
|
||||
pub fn into_legacy_and_keypair(self) -> (LegacyAuthenticatorClient, KeyPair) {
|
||||
(
|
||||
LegacyAuthenticatorClient {
|
||||
public_key: *self.keypair.public_key(),
|
||||
mixnet_listener: self.mixnet_listener,
|
||||
mixnet_sender: self.mixnet_sender,
|
||||
our_nym_address: self.our_nym_address,
|
||||
auth_recipient: self.auth_recipient,
|
||||
auth_version: self.auth_version,
|
||||
},
|
||||
self.keypair,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// This is the legacy Authenticator that has to be used to handle bandwidth top up for legacy gateaways
|
||||
pub struct LegacyAuthenticatorClient {
|
||||
public_key: PublicKey,
|
||||
mixnet_listener: MixnetMessageBroadcastReceiver,
|
||||
mixnet_sender: MixnetMessageInputSender,
|
||||
our_nym_address: Recipient,
|
||||
pub auth_recipient: Recipient,
|
||||
auth_version: AuthenticatorVersion,
|
||||
}
|
||||
|
||||
impl LegacyAuthenticatorClient {
|
||||
pub async fn send_and_wait_for_response(
|
||||
&mut self,
|
||||
message: &ClientMessage,
|
||||
) -> Result<AuthenticatorResponse> {
|
||||
let request_id = self.send_request(message).await?;
|
||||
|
||||
debug!("Waiting for reply...");
|
||||
self.listen_for_response(request_id).await
|
||||
}
|
||||
|
||||
async fn send_request(&self, message: &ClientMessage) -> Result<u64> {
|
||||
let (data, request_id) = message.bytes(self.our_nym_address)?;
|
||||
|
||||
// We use 20 surbs for the connect request because typically the
|
||||
// authenticator mixnet client on the nym-node is configured to have a min
|
||||
// threshold of 10 surbs that it reserves for itself to request additional
|
||||
// surbs.
|
||||
let surbs = if message.use_surbs() {
|
||||
match &message {
|
||||
ClientMessage::Initial(_) => IncludedSurbs::new(20),
|
||||
_ => IncludedSurbs::new(1),
|
||||
}
|
||||
} else {
|
||||
IncludedSurbs::ExposeSelfAddress
|
||||
};
|
||||
let input_message = helpers::create_input_message(self.auth_recipient, data, surbs);
|
||||
|
||||
self.mixnet_sender
|
||||
.send(input_message)
|
||||
.await
|
||||
.map_err(|e| Error::SendMixnetMessage(Box::new(e)))?;
|
||||
|
||||
Ok(request_id)
|
||||
}
|
||||
|
||||
async fn listen_for_response(&mut self, request_id: u64) -> Result<AuthenticatorResponse> {
|
||||
let timeout = tokio::time::sleep(Duration::from_secs(10));
|
||||
tokio::pin!(timeout);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = &mut timeout => {
|
||||
error!("Timed out waiting for reply to connect request");
|
||||
return Err(Error::TimeoutWaitingForConnectResponse);
|
||||
}
|
||||
msg = self.mixnet_listener.recv() => match msg {
|
||||
Err(_) => {
|
||||
return Err(Error::NoMixnetMessagesReceived);
|
||||
}
|
||||
Ok(msg) => {
|
||||
let Some(header) = msg.message.first_chunk::<2>() else {
|
||||
debug!("received too short message that couldn't have been from the authenticator while waiting for connect response");
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(protocol) = Protocol::try_from(header) else {
|
||||
debug!("received a message not meant to any service provider while waiting for connect response");
|
||||
continue;
|
||||
};
|
||||
|
||||
if !protocol.service_provider_type.is_authenticator() {
|
||||
debug!("Received non-authenticator message while waiting for connect response");
|
||||
continue;
|
||||
}
|
||||
// Confirm that the version is correct
|
||||
let version = AuthenticatorVersion::from(protocol.version);
|
||||
|
||||
// Then we deserialize the message
|
||||
debug!("AuthClient: got message while waiting for connect response with version {version:?}");
|
||||
let ret: Result<AuthenticatorResponse> = match version {
|
||||
AuthenticatorVersion::V1 => Err(Error::UnsupportedVersion),
|
||||
AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
|
||||
AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
|
||||
AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
|
||||
AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
|
||||
AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion),
|
||||
};
|
||||
let Ok(response) = ret else {
|
||||
// This is ok, it's likely just one of our self-pings
|
||||
debug!("Failed to deserialize reconstructed message");
|
||||
continue;
|
||||
};
|
||||
|
||||
if response.id() == request_id {
|
||||
debug!("Got response with matching id");
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn query_bandwidth(&mut self) -> Result<Option<i64>> {
|
||||
let query_message = match self.auth_version {
|
||||
AuthenticatorVersion::V1 => return Err(Error::UnsupportedAuthenticatorVersion),
|
||||
AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl {
|
||||
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
|
||||
version: AuthenticatorVersion::V2,
|
||||
})),
|
||||
AuthenticatorVersion::V3 => ClientMessage::Query(Box::new(QueryMessageImpl {
|
||||
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
|
||||
version: AuthenticatorVersion::V3,
|
||||
})),
|
||||
AuthenticatorVersion::V4 => ClientMessage::Query(Box::new(QueryMessageImpl {
|
||||
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
|
||||
version: AuthenticatorVersion::V4,
|
||||
})),
|
||||
AuthenticatorVersion::V5 => ClientMessage::Query(Box::new(QueryMessageImpl {
|
||||
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
|
||||
version: AuthenticatorVersion::V5,
|
||||
})),
|
||||
AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion),
|
||||
};
|
||||
let response = self.send_and_wait_for_response(&query_message).await?;
|
||||
|
||||
let available_bandwidth = match response {
|
||||
AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => {
|
||||
if let Some(available_bandwidth) =
|
||||
remaining_bandwidth_response.available_bandwidth()
|
||||
{
|
||||
available_bandwidth
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
_ => return Err(Error::InvalidGatewayAuthResponse),
|
||||
};
|
||||
|
||||
let remaining_pretty = if available_bandwidth > 1024 * 1024 {
|
||||
format!("{:.2} MB", available_bandwidth as f64 / 1024.0 / 1024.0)
|
||||
} else {
|
||||
format!("{} KB", available_bandwidth / 1024)
|
||||
};
|
||||
tracing::debug!(
|
||||
"Remaining wireguard bandwidth with gateway {} for today: {}",
|
||||
self.auth_recipient.gateway(),
|
||||
remaining_pretty
|
||||
);
|
||||
if available_bandwidth < 1024 * 1024 {
|
||||
tracing::warn!(
|
||||
"Remaining bandwidth is under 1 MB. The wireguard mode will get suspended after that until tomorrow, UTC time. The client might shutdown with timeout soon"
|
||||
);
|
||||
}
|
||||
Ok(Some(available_bandwidth))
|
||||
}
|
||||
|
||||
pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result<i64> {
|
||||
let top_up_message = match self.auth_version {
|
||||
AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage {
|
||||
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
|
||||
credential,
|
||||
})),
|
||||
// NOTE: looks like a bug here using v3. But we're leaving it as is since it's working
|
||||
// and V4 is deprecated in favour of V5
|
||||
AuthenticatorVersion::V4 => ClientMessage::TopUp(Box::new(v4::topup::TopUpMessage {
|
||||
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
|
||||
credential,
|
||||
})),
|
||||
AuthenticatorVersion::V5 => ClientMessage::TopUp(Box::new(v5::topup::TopUpMessage {
|
||||
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
|
||||
credential,
|
||||
})),
|
||||
AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => {
|
||||
return Err(Error::UnsupportedAuthenticatorVersion);
|
||||
}
|
||||
};
|
||||
let response = self.send_and_wait_for_response(&top_up_message).await?;
|
||||
|
||||
let remaining_bandwidth = match response {
|
||||
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
|
||||
top_up_bandwidth_response.available_bandwidth()
|
||||
}
|
||||
_ => return Err(Error::InvalidGatewayAuthResponse),
|
||||
};
|
||||
|
||||
Ok(remaining_bandwidth)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_authenticator_requests::client_message::QueryMessageImpl;
|
||||
use nym_bandwidth_controller::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
|
||||
use nym_crypto::asymmetric::x25519::KeyPair;
|
||||
use nym_registration_common::GatewayData;
|
||||
use nym_registration_common::{
|
||||
GatewayData, DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME,
|
||||
DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME, DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME,
|
||||
DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME,
|
||||
};
|
||||
use rand::rngs::OsRng;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
@@ -16,16 +19,20 @@ use nym_authenticator_requests::{
|
||||
client_message::ClientMessage, response::AuthenticatorResponse, traits::Id, v2, v3, v4, v5,
|
||||
AuthenticatorVersion,
|
||||
};
|
||||
use nym_credentials_interface::{CredentialSpendingData, TicketType};
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_crypto::asymmetric::x25519::KeyPair;
|
||||
use nym_pemstore::KeyPairPath;
|
||||
use nym_sdk::mixnet::{IncludedSurbs, Recipient};
|
||||
use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt};
|
||||
use nym_wireguard_types::PeerPublicKey;
|
||||
|
||||
mod error;
|
||||
mod helpers;
|
||||
mod legacy;
|
||||
mod mixnet_listener;
|
||||
|
||||
pub use crate::error::{Error, Result};
|
||||
pub use crate::legacy::LegacyAuthenticatorClient;
|
||||
pub use crate::mixnet_listener::{AuthClientMixnetListener, AuthClientMixnetListenerHandle};
|
||||
|
||||
pub struct AuthenticatorClient {
|
||||
@@ -35,21 +42,34 @@ pub struct AuthenticatorClient {
|
||||
pub auth_recipient: Recipient,
|
||||
auth_version: AuthenticatorVersion,
|
||||
|
||||
keypair: Arc<KeyPair>,
|
||||
keypair: KeyPair,
|
||||
ip_addr: IpAddr,
|
||||
}
|
||||
|
||||
impl AuthenticatorClient {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
fn new_type(
|
||||
data_path: &Option<PathBuf>,
|
||||
mixnet_listener: MixnetMessageBroadcastReceiver,
|
||||
mixnet_sender: MixnetMessageInputSender,
|
||||
our_nym_address: Recipient,
|
||||
auth_recipient: Recipient,
|
||||
auth_version: AuthenticatorVersion,
|
||||
keypair: Arc<KeyPair>,
|
||||
private_file_name: &str,
|
||||
public_file_name: &str,
|
||||
ip_addr: IpAddr,
|
||||
) -> Self {
|
||||
let mut rng = OsRng;
|
||||
|
||||
let keypair = if let Some(data_path) = data_path {
|
||||
let paths = KeyPairPath::new(
|
||||
data_path.join(private_file_name),
|
||||
data_path.join(public_file_name),
|
||||
);
|
||||
helpers::load_or_generate_keypair(&mut rng, paths)
|
||||
} else {
|
||||
KeyPair::new(&mut rng)
|
||||
};
|
||||
Self {
|
||||
mixnet_listener,
|
||||
mixnet_sender,
|
||||
@@ -61,6 +81,50 @@ impl AuthenticatorClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_entry(
|
||||
data_path: &Option<PathBuf>,
|
||||
mixnet_listener: MixnetMessageBroadcastReceiver,
|
||||
mixnet_sender: MixnetMessageInputSender,
|
||||
our_nym_address: Recipient,
|
||||
auth_recipient: Recipient,
|
||||
auth_version: AuthenticatorVersion,
|
||||
ip_addr: IpAddr,
|
||||
) -> Self {
|
||||
Self::new_type(
|
||||
data_path,
|
||||
mixnet_listener,
|
||||
mixnet_sender,
|
||||
our_nym_address,
|
||||
auth_recipient,
|
||||
auth_version,
|
||||
DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME,
|
||||
DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME,
|
||||
ip_addr,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_exit(
|
||||
data_path: &Option<PathBuf>,
|
||||
mixnet_listener: MixnetMessageBroadcastReceiver,
|
||||
mixnet_sender: MixnetMessageInputSender,
|
||||
our_nym_address: Recipient,
|
||||
auth_recipient: Recipient,
|
||||
auth_version: AuthenticatorVersion,
|
||||
ip_addr: IpAddr,
|
||||
) -> Self {
|
||||
Self::new_type(
|
||||
data_path,
|
||||
mixnet_listener,
|
||||
mixnet_sender,
|
||||
our_nym_address,
|
||||
auth_recipient,
|
||||
auth_version,
|
||||
DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME,
|
||||
DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME,
|
||||
ip_addr,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn send_and_wait_for_response(
|
||||
&mut self,
|
||||
message: &ClientMessage,
|
||||
@@ -301,91 +365,4 @@ impl AuthenticatorClient {
|
||||
|
||||
Ok(gateway_data)
|
||||
}
|
||||
|
||||
pub async fn query_bandwidth(&mut self) -> Result<Option<i64>> {
|
||||
let query_message = match self.auth_version {
|
||||
AuthenticatorVersion::V1 => return Err(Error::UnsupportedAuthenticatorVersion),
|
||||
AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl {
|
||||
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
|
||||
version: AuthenticatorVersion::V2,
|
||||
})),
|
||||
AuthenticatorVersion::V3 => ClientMessage::Query(Box::new(QueryMessageImpl {
|
||||
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
|
||||
version: AuthenticatorVersion::V3,
|
||||
})),
|
||||
AuthenticatorVersion::V4 => ClientMessage::Query(Box::new(QueryMessageImpl {
|
||||
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
|
||||
version: AuthenticatorVersion::V4,
|
||||
})),
|
||||
AuthenticatorVersion::V5 => ClientMessage::Query(Box::new(QueryMessageImpl {
|
||||
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
|
||||
version: AuthenticatorVersion::V5,
|
||||
})),
|
||||
AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion),
|
||||
};
|
||||
let response = self.send_and_wait_for_response(&query_message).await?;
|
||||
|
||||
let available_bandwidth = match response {
|
||||
AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => {
|
||||
if let Some(available_bandwidth) =
|
||||
remaining_bandwidth_response.available_bandwidth()
|
||||
{
|
||||
available_bandwidth
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
_ => return Err(Error::InvalidGatewayAuthResponse),
|
||||
};
|
||||
|
||||
let remaining_pretty = if available_bandwidth > 1024 * 1024 {
|
||||
format!("{:.2} MB", available_bandwidth as f64 / 1024.0 / 1024.0)
|
||||
} else {
|
||||
format!("{} KB", available_bandwidth / 1024)
|
||||
};
|
||||
tracing::debug!(
|
||||
"Remaining wireguard bandwidth with gateway {} for today: {}",
|
||||
self.auth_recipient.gateway(),
|
||||
remaining_pretty
|
||||
);
|
||||
if available_bandwidth < 1024 * 1024 {
|
||||
tracing::warn!(
|
||||
"Remaining bandwidth is under 1 MB. The wireguard mode will get suspended after that until tomorrow, UTC time. The client might shutdown with timeout soon
|
||||
"
|
||||
);
|
||||
}
|
||||
Ok(Some(available_bandwidth))
|
||||
}
|
||||
|
||||
pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result<i64> {
|
||||
let top_up_message = match self.auth_version {
|
||||
AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage {
|
||||
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
|
||||
credential,
|
||||
})),
|
||||
// NOTE: looks like a bug here using v3. But we're leaving it as is since it's working
|
||||
// and V4 is deprecated in favour of V5
|
||||
AuthenticatorVersion::V4 => ClientMessage::TopUp(Box::new(v4::topup::TopUpMessage {
|
||||
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
|
||||
credential,
|
||||
})),
|
||||
AuthenticatorVersion::V5 => ClientMessage::TopUp(Box::new(v5::topup::TopUpMessage {
|
||||
pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()),
|
||||
credential,
|
||||
})),
|
||||
AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => {
|
||||
return Err(Error::UnsupportedAuthenticatorVersion);
|
||||
}
|
||||
};
|
||||
let response = self.send_and_wait_for_response(&top_up_message).await?;
|
||||
|
||||
let remaining_bandwidth = match response {
|
||||
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
|
||||
top_up_bandwidth_response.available_bandwidth()
|
||||
}
|
||||
_ => return Err(Error::InvalidGatewayAuthResponse),
|
||||
};
|
||||
|
||||
Ok(remaining_bandwidth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-credential-proxy"
|
||||
version = "0.3.0"
|
||||
version = "0.2.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
@@ -12,6 +12,7 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.workspace = true
|
||||
tokio-util.workspace = true
|
||||
|
||||
@@ -5,31 +5,24 @@ use nym_credential_storage::persistent_storage::PersistentStorage;
|
||||
use nym_registration_common::NymNode;
|
||||
use nym_sdk::{
|
||||
mixnet::{
|
||||
x25519::KeyPair, CredentialStorage, GatewaysDetailsStore, KeyStore, MixnetClient,
|
||||
MixnetClientBuilder, MixnetClientStorage, OnDiskPersistent, ReplyStorageBackend,
|
||||
StoragePaths,
|
||||
CredentialStorage, GatewaysDetailsStore, KeyStore, MixnetClient, MixnetClientBuilder,
|
||||
MixnetClientStorage, OnDiskPersistent, ReplyStorageBackend, StoragePaths,
|
||||
},
|
||||
DebugConfig, NymNetworkDetails, RememberMe, TopologyProvider, UserAgent,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::fd::RawFd;
|
||||
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{os::fd::RawFd, sync::Arc};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::error::RegistrationClientError;
|
||||
|
||||
const VPN_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(15);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NymNodeWithKeys {
|
||||
pub node: NymNode,
|
||||
pub keys: Arc<KeyPair>,
|
||||
}
|
||||
|
||||
pub struct BuilderConfig {
|
||||
pub entry_node: NymNodeWithKeys,
|
||||
pub exit_node: NymNodeWithKeys,
|
||||
pub entry_node: NymNode,
|
||||
pub exit_node: NymNode,
|
||||
pub data_path: Option<PathBuf>,
|
||||
pub mixnet_client_config: MixnetClientConfig,
|
||||
pub two_hops: bool,
|
||||
@@ -111,13 +104,12 @@ impl BuilderConfig {
|
||||
|
||||
let builder = builder
|
||||
.with_user_agent(self.user_agent)
|
||||
.request_gateway(self.entry_node.node.identity.to_string())
|
||||
.request_gateway(self.entry_node.identity.to_string())
|
||||
.network_details(self.network_env)
|
||||
.debug_config(debug_config)
|
||||
.credentials_mode(true)
|
||||
.with_remember_me(remember_me)
|
||||
.custom_topology_provider(self.custom_topology_provider);
|
||||
|
||||
#[cfg(unix)]
|
||||
let builder = builder.with_connection_fd_callback(self.connection_fd_callback);
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use futures::channel::mpsc;
|
||||
use nym_bandwidth_controller::{BandwidthController, BandwidthTicketProvider};
|
||||
use nym_credential_storage::ephemeral_storage::EphemeralCredentialStorage;
|
||||
use nym_sdk::{
|
||||
mixnet::{MixnetClient, MixnetClientBuilder},
|
||||
mixnet::{EventSender, MixnetClient, MixnetClientBuilder},
|
||||
NymNetworkDetails,
|
||||
};
|
||||
use nym_validator_client::{
|
||||
@@ -32,11 +33,13 @@ impl RegistrationClientBuilder {
|
||||
pub async fn build(self) -> Result<RegistrationClient, RegistrationClientError> {
|
||||
let storage = self.config.setup_storage().await?;
|
||||
let config = RegistrationClientConfig {
|
||||
entry: self.config.entry_node.clone(),
|
||||
exit: self.config.exit_node.clone(),
|
||||
entry: self.config.entry_node,
|
||||
exit: self.config.exit_node,
|
||||
two_hops: self.config.two_hops,
|
||||
data_path: self.config.data_path.clone(),
|
||||
};
|
||||
let cancel_token = self.config.cancel_token.clone();
|
||||
let (event_tx, event_rx) = mpsc::unbounded();
|
||||
|
||||
let nyxd_client = get_nyxd_client(&self.config.network_env)?;
|
||||
|
||||
@@ -44,7 +47,8 @@ impl RegistrationClientBuilder {
|
||||
MixnetClient,
|
||||
Box<dyn BandwidthTicketProvider>,
|
||||
) = if let Some((mixnet_client_storage, credential_storage)) = storage {
|
||||
let builder = MixnetClientBuilder::new_with_storage(mixnet_client_storage);
|
||||
let builder = MixnetClientBuilder::new_with_storage(mixnet_client_storage)
|
||||
.event_tx(EventSender(event_tx));
|
||||
let mixnet_client = tokio::time::timeout(
|
||||
MIXNET_CLIENT_STARTUP_TIMEOUT,
|
||||
self.config.build_and_connect_mixnet_client(builder),
|
||||
@@ -54,7 +58,7 @@ impl RegistrationClientBuilder {
|
||||
Box::new(BandwidthController::new(credential_storage, nyxd_client));
|
||||
(mixnet_client, bandwidth_controller)
|
||||
} else {
|
||||
let builder = MixnetClientBuilder::new_ephemeral();
|
||||
let builder = MixnetClientBuilder::new_ephemeral().event_tx(EventSender(event_tx));
|
||||
let mixnet_client = tokio::time::timeout(
|
||||
MIXNET_CLIENT_STARTUP_TIMEOUT,
|
||||
self.config.build_and_connect_mixnet_client(builder),
|
||||
@@ -74,6 +78,7 @@ impl RegistrationClientBuilder {
|
||||
cancel_token,
|
||||
mixnet_client_address,
|
||||
bandwidth_controller,
|
||||
event_rx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::builder::config::NymNodeWithKeys;
|
||||
use nym_registration_common::NymNode;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct RegistrationClientConfig {
|
||||
pub(crate) entry: NymNodeWithKeys,
|
||||
pub(crate) exit: NymNodeWithKeys,
|
||||
pub(crate) entry: NymNode,
|
||||
pub(crate) exit: NymNode,
|
||||
pub(crate) two_hops: bool,
|
||||
pub(crate) data_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use nym_bandwidth_controller::BandwidthTicketProvider;
|
||||
use nym_credentials_interface::TicketType;
|
||||
use nym_ip_packet_client::IprClientConnect;
|
||||
use nym_registration_common::AssignedAddresses;
|
||||
use nym_sdk::mixnet::{MixnetClient, Recipient};
|
||||
use nym_sdk::mixnet::{EventReceiver, MixnetClient, Recipient};
|
||||
|
||||
use crate::config::RegistrationClientConfig;
|
||||
|
||||
@@ -17,10 +17,7 @@ mod config;
|
||||
mod error;
|
||||
mod types;
|
||||
|
||||
pub use builder::config::{
|
||||
BuilderConfig as RegistrationClientBuilderConfig, MixnetClientConfig,
|
||||
NymNodeWithKeys as RegistrationNymNode,
|
||||
};
|
||||
pub use builder::config::{BuilderConfig as RegistrationClientBuilderConfig, MixnetClientConfig};
|
||||
pub use builder::RegistrationClientBuilder;
|
||||
pub use error::RegistrationClientError;
|
||||
pub use types::{MixnetRegistrationResult, RegistrationResult, WireguardRegistrationResult};
|
||||
@@ -31,17 +28,18 @@ pub struct RegistrationClient {
|
||||
mixnet_client_address: Recipient,
|
||||
bandwidth_controller: Box<dyn BandwidthTicketProvider>,
|
||||
cancel_token: CancellationToken,
|
||||
event_rx: EventReceiver,
|
||||
}
|
||||
|
||||
impl RegistrationClient {
|
||||
async fn register_mix_exit(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let entry_mixnet_gateway_ip = self.config.entry.node.ip_address;
|
||||
let entry_mixnet_gateway_ip = self.config.entry.ip_address;
|
||||
|
||||
let exit_mixnet_gateway_ip = self.config.exit.node.ip_address;
|
||||
let exit_mixnet_gateway_ip = self.config.exit.ip_address;
|
||||
|
||||
let ipr_address = self.config.exit.node.ipr_address.ok_or(
|
||||
let ipr_address = self.config.exit.ipr_address.ok_or(
|
||||
RegistrationClientError::NoIpPacketRouterAddress {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
node_id: self.config.exit.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
let mut ipr_client =
|
||||
@@ -61,26 +59,27 @@ impl RegistrationClient {
|
||||
entry_mixnet_gateway_ip,
|
||||
exit_mixnet_gateway_ip,
|
||||
},
|
||||
event_rx: self.event_rx,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
async fn register_wg(self) -> Result<RegistrationResult, RegistrationClientError> {
|
||||
let entry_auth_address = self.config.entry.node.authenticator_address.ok_or(
|
||||
let entry_auth_address = self.config.entry.authenticator_address.ok_or(
|
||||
RegistrationClientError::AuthenticationNotPossible {
|
||||
node_id: self.config.entry.node.identity.to_base58_string(),
|
||||
node_id: self.config.entry.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let exit_auth_address = self.config.exit.node.authenticator_address.ok_or(
|
||||
let exit_auth_address = self.config.exit.authenticator_address.ok_or(
|
||||
RegistrationClientError::AuthenticationNotPossible {
|
||||
node_id: self.config.exit.node.identity.to_base58_string(),
|
||||
node_id: self.config.exit.identity.to_base58_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let entry_version = self.config.entry.node.version;
|
||||
let entry_version = self.config.entry.version;
|
||||
tracing::debug!("Entry gateway version: {entry_version}");
|
||||
let exit_version = self.config.exit.node.version;
|
||||
let exit_version = self.config.exit.version;
|
||||
tracing::debug!("Exit gateway version: {exit_version}");
|
||||
|
||||
// Start the auth client mixnet listener, which will listen for incoming messages from the
|
||||
@@ -88,24 +87,24 @@ impl RegistrationClient {
|
||||
let mixnet_listener =
|
||||
AuthClientMixnetListener::new(self.mixnet_client, self.cancel_token.clone()).start();
|
||||
|
||||
let mut entry_auth_client = AuthenticatorClient::new(
|
||||
let mut entry_auth_client = AuthenticatorClient::new_entry(
|
||||
&self.config.data_path,
|
||||
mixnet_listener.subscribe(),
|
||||
mixnet_listener.mixnet_sender(),
|
||||
self.mixnet_client_address,
|
||||
entry_auth_address,
|
||||
entry_version,
|
||||
self.config.entry.keys,
|
||||
self.config.entry.node.ip_address,
|
||||
self.config.entry.ip_address,
|
||||
);
|
||||
|
||||
let mut exit_auth_client = AuthenticatorClient::new(
|
||||
let mut exit_auth_client = AuthenticatorClient::new_exit(
|
||||
&self.config.data_path,
|
||||
mixnet_listener.subscribe(),
|
||||
mixnet_listener.mixnet_sender(),
|
||||
self.mixnet_client_address,
|
||||
exit_auth_address,
|
||||
exit_version,
|
||||
self.config.exit.keys,
|
||||
self.config.exit.node.ip_address,
|
||||
self.config.exit.ip_address,
|
||||
);
|
||||
|
||||
let entry_fut = entry_auth_client
|
||||
@@ -118,7 +117,7 @@ impl RegistrationClient {
|
||||
let entry =
|
||||
entry.map_err(
|
||||
|source| RegistrationClientError::EntryGatewayRegisterWireguard {
|
||||
gateway_id: self.config.entry.node.identity.to_base58_string(),
|
||||
gateway_id: self.config.entry.identity.to_base58_string(),
|
||||
authenticator_address: Box::new(entry_auth_address),
|
||||
source: Box::new(source),
|
||||
},
|
||||
@@ -126,7 +125,7 @@ impl RegistrationClient {
|
||||
let exit =
|
||||
exit.map_err(
|
||||
|source| RegistrationClientError::ExitGatewayRegisterWireguard {
|
||||
gateway_id: self.config.exit.node.identity.to_base58_string(),
|
||||
gateway_id: self.config.exit.identity.to_base58_string(),
|
||||
authenticator_address: Box::new(exit_auth_address),
|
||||
source: Box::new(source),
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use nym_authenticator_client::{AuthClientMixnetListenerHandle, AuthenticatorClient};
|
||||
use nym_bandwidth_controller::BandwidthTicketProvider;
|
||||
use nym_registration_common::{AssignedAddresses, GatewayData};
|
||||
use nym_sdk::mixnet::MixnetClient;
|
||||
use nym_sdk::mixnet::{EventReceiver, MixnetClient};
|
||||
|
||||
pub enum RegistrationResult {
|
||||
Mixnet(Box<MixnetRegistrationResult>),
|
||||
@@ -14,6 +14,7 @@ pub enum RegistrationResult {
|
||||
pub struct MixnetRegistrationResult {
|
||||
pub assigned_addresses: AssignedAddresses,
|
||||
pub mixnet_client: MixnetClient,
|
||||
pub event_rx: EventReceiver,
|
||||
}
|
||||
|
||||
pub struct WireguardRegistrationResult {
|
||||
|
||||
@@ -85,41 +85,26 @@ apply_iptables_rules() {
|
||||
echo "applying IPtables rules for $interface..."
|
||||
sleep 2
|
||||
|
||||
# INPUT rules - allow incoming connections TO the gateway from tunnel clients
|
||||
# This is CRITICAL for mobile clients to reach the bandwidth controller at 10.1.0.1:51830
|
||||
sudo iptables -I INPUT -i "$interface" -j ACCEPT
|
||||
sudo ip6tables -I INPUT -i "$interface" -j ACCEPT
|
||||
|
||||
# NAT rules - for outbound traffic masquerading
|
||||
sudo iptables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE
|
||||
sudo ip6tables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE
|
||||
|
||||
# FORWARD rules - allow traffic through the gateway
|
||||
sudo iptables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT
|
||||
sudo iptables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
|
||||
sudo ip6tables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE
|
||||
sudo ip6tables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT
|
||||
sudo ip6tables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
|
||||
sudo iptables-save | sudo tee /etc/iptables/rules.v4
|
||||
sudo ip6tables-save | sudo tee /etc/iptables/rules.v6
|
||||
|
||||
echo "IPtables rules applied successfully for $interface (including INPUT rules for bandwidth controller)."
|
||||
}
|
||||
|
||||
check_tunnel_iptables() {
|
||||
local interface=$1
|
||||
echo "inspecting IPtables rules for $interface..."
|
||||
echo "---------------------------------------"
|
||||
echo "IPv4 INPUT rules (for bandwidth controller):"
|
||||
iptables -L INPUT -v -n | grep -E "$interface|Chain INPUT" | head -20
|
||||
echo "---------------------------------------"
|
||||
echo "IPv4 FORWARD rules:"
|
||||
echo "IPv4 rules:"
|
||||
iptables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw-reject-forward"'
|
||||
echo "---------------------------------------"
|
||||
echo "IPv6 INPUT rules (for bandwidth controller):"
|
||||
ip6tables -L INPUT -v -n | grep -E "$interface|Chain INPUT" | head -20
|
||||
echo "---------------------------------------"
|
||||
echo "IPv6 FORWARD rules:"
|
||||
echo "IPv6 rules:"
|
||||
ip6tables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw6-reject-forward"'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
__version__ = "1.1.0"
|
||||
__version__ = "1.0.0"
|
||||
__default_branch__ = "develop"
|
||||
|
||||
import os
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
address,tag
|
||||
add_wallet_or_delete,optional_tag_or_delete
|
||||
add_wallet_or_delete,optional_tag_or_delete
|
||||
add_wallet_or_delete,optional_tag_or_delete
|
||||
add_wallet_or_delete,optional_tag_or_delete
|
||||
add_wallet_or_delete,optional_tag_or_delete
|
||||
|
@@ -1,627 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
This script fetches operators rewards based on provided Nyx account addresses provided in data/wallet-addresses.csv.
|
||||
Output is:
|
||||
1. Printet table in terminal
|
||||
2. Sheet with complete info stored in data/node-balances.csv
|
||||
3. Hiostorical data yaml file stored in data/data.yaml - this file should not be changed by hand, as
|
||||
all values older than 30 days get auto-removed
|
||||
Before you start fill first column of data/wallet-addresses with your Nyx account addresses and (optionally) second column
|
||||
with a tag, for example "mysquad" and "personal" to get sorted output per entity.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import yaml
|
||||
import requests
|
||||
from collections import defaultdict
|
||||
from typing import Any, Dict, List, Tuple, Optional
|
||||
from tabulate import tabulate
|
||||
from colorama import init as colorama_init, Fore, Style
|
||||
|
||||
colorama_init()
|
||||
|
||||
DATA_DIR = os.path.join(os.getcwd(), "data")
|
||||
ADDR_CSV = os.path.join(DATA_DIR, "wallet-addresses.csv")
|
||||
OUT_CSV = os.path.join(DATA_DIR, "node-balances.csv")
|
||||
HIST_FILE = os.path.join(DATA_DIR, "data.yaml")
|
||||
|
||||
SPECTRE_NODES_URL = "https://api.nym.spectredao.net/api/v1/nodes"
|
||||
VALIDATOR_BONDED_URL = "https://validator.nymtech.net/api/v1/nym-nodes/bonded"
|
||||
VALIDATOR_DESC_URL = "https://validator.nymtech.net/api/v1/nym-nodes/described"
|
||||
SPECTRE_BAL_URL = "https://api.nym.spectredao.net/api/v1/balances/{address}"
|
||||
|
||||
SESSION = requests.Session()
|
||||
SESSION.headers.update({"User-Agent": "nym-tools/1.0"})
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
print(msg, flush=True)
|
||||
|
||||
|
||||
def now_ts() -> float:
|
||||
return time.time()
|
||||
|
||||
|
||||
def to_float(x: Any, default: float = 0.0) -> float:
|
||||
try:
|
||||
if x is None:
|
||||
return default
|
||||
if isinstance(x, (int, float)):
|
||||
return float(x)
|
||||
if isinstance(x, str):
|
||||
return float(x)
|
||||
return default
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def to_int(x: Any, default: int = 0) -> int:
|
||||
try:
|
||||
if x is None:
|
||||
return default
|
||||
if isinstance(x, int):
|
||||
return x
|
||||
if isinstance(x, float):
|
||||
return int(x)
|
||||
if isinstance(x, str):
|
||||
return int(float(x))
|
||||
return default
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
# pagination helpers
|
||||
|
||||
def _get_json(url: str, params: Dict[str, Any], timeout: int = 60) -> Any:
|
||||
r = SESSION.get(url, params=params, timeout=timeout)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def _fetch_all_limit_offset(url: str, limit: int = 1000, timeout: int = 60) -> List[Any]:
|
||||
out: List[Any] = []
|
||||
offset = 0
|
||||
tries = 0
|
||||
while True:
|
||||
tries += 1
|
||||
data = _get_json(url, {"limit": limit, "offset": offset}, timeout=timeout)
|
||||
if isinstance(data, dict) and "data" in data and isinstance(data["data"], list):
|
||||
items = data["data"]
|
||||
elif isinstance(data, list):
|
||||
items = data
|
||||
else:
|
||||
break
|
||||
if not items:
|
||||
break
|
||||
out.extend(items)
|
||||
if len(items) < limit:
|
||||
break
|
||||
offset += limit
|
||||
if tries > 500:
|
||||
break
|
||||
return out
|
||||
|
||||
def _fetch_all_page_pagesize(url: str, page_size: int = 1000, timeout: int = 60) -> List[Any]:
|
||||
out: List[Any] = []
|
||||
page = 0
|
||||
tries = 0
|
||||
while True:
|
||||
tries += 1
|
||||
data = _get_json(url, {"page": page, "size": page_size}, timeout=timeout)
|
||||
if isinstance(data, dict) and "data" in data and isinstance(data["data"], list):
|
||||
items = data["data"]
|
||||
elif isinstance(data, list):
|
||||
items = data
|
||||
else:
|
||||
break
|
||||
out.extend(items)
|
||||
total = to_int(data.get("pagination", {}).get("total"), -1) if isinstance(data, dict) else -1
|
||||
if total >= 0 and len(out) >= total:
|
||||
break
|
||||
if not items:
|
||||
break
|
||||
page += 1
|
||||
if tries > 500:
|
||||
break
|
||||
return out
|
||||
|
||||
def _fetch_all_single(url: str, timeout: int = 60) -> List[Any]:
|
||||
data = _get_json(url, {}, timeout=timeout)
|
||||
if isinstance(data, dict) and "data" in data and isinstance(data["data"], list):
|
||||
return data["data"]
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
return []
|
||||
|
||||
def _fetch_all_any(url: str, timeout: int = 60) -> list:
|
||||
got = _fetch_all_limit_offset(url, limit=1000, timeout=timeout)
|
||||
if isinstance(got, list) and got:
|
||||
return got
|
||||
got = _fetch_all_page_pagesize(url, page_size=1000, timeout=timeout)
|
||||
if isinstance(got, list) and got:
|
||||
return got
|
||||
return _fetch_all_single(url, timeout=timeout)
|
||||
|
||||
|
||||
# load data
|
||||
|
||||
def read_wallets_csv(path: str) -> List[Tuple[str, str]]:
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"Input CSV not found: {path}")
|
||||
rows: List[Tuple[str, str]] = []
|
||||
with open(path, newline="", encoding="utf-8") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
addr = (row.get("address") or "").strip()
|
||||
tag = (row.get("tag") or "").strip()
|
||||
if addr:
|
||||
rows.append((addr, tag))
|
||||
return rows
|
||||
|
||||
def fetch_nodes_all() -> List[Dict[str, Any]]:
|
||||
log(f"* * * Fetching nodes from {SPECTRE_NODES_URL} * * *")
|
||||
nodes = _fetch_all_any(SPECTRE_NODES_URL, timeout=90)
|
||||
log(f"Fetched {len(nodes)} node(s)")
|
||||
return nodes
|
||||
|
||||
def fetch_bonded_all() -> List[Dict[str, Any]]:
|
||||
log(f"* * *Fetching bonded from {VALIDATOR_BONDED_URL} * * *")
|
||||
bonded = _fetch_all_any(VALIDATOR_BONDED_URL, timeout=90)
|
||||
log(f"Fetched {len(bonded)} bonded record(s)")
|
||||
return bonded
|
||||
|
||||
def fetch_described_all() -> List[Dict[str, Any]]:
|
||||
log(f"* * * Fetching described from {VALIDATOR_DESC_URL} * * *")
|
||||
described = _fetch_all_any(VALIDATOR_DESC_URL, timeout=90)
|
||||
log(f"Fetched {len(described)} described record(s)")
|
||||
return described
|
||||
|
||||
def fetch_balance_total_nym(address: str) -> float:
|
||||
url = SPECTRE_BAL_URL.format(address=address)
|
||||
try:
|
||||
r = SESSION.get(url, timeout=30)
|
||||
r.raise_for_status()
|
||||
js = r.json()
|
||||
amt = to_float(js.get("total", {}).get("amount"), 0.0)
|
||||
return amt / 1_000_000.0
|
||||
except Exception as e:
|
||||
log(f"{Fore.YELLOW}* * * warn: balance fetch failed for {address}: {e}{Style.RESET_ALL} * * *")
|
||||
return 0.0
|
||||
|
||||
|
||||
# extract version
|
||||
|
||||
def _first_str(*vals) -> str:
|
||||
for v in vals:
|
||||
if isinstance(v, (str, int, float)):
|
||||
s = str(v).strip()
|
||||
if s:
|
||||
return s
|
||||
return ""
|
||||
|
||||
def extract_version_from_node(n: Dict[str, Any]) -> str:
|
||||
desc = n.get("description") or {}
|
||||
bi = n.get("build_information") or {}
|
||||
return _first_str(
|
||||
n.get("version"),
|
||||
n.get("node_version"),
|
||||
bi.get("build_version"),
|
||||
bi.get("version"),
|
||||
(desc.get("software") or {}).get("version"),
|
||||
(desc.get("build_information") or {}).get("build_version"),
|
||||
)
|
||||
|
||||
def extract_version_from_desc(d: Dict[str, Any]) -> str:
|
||||
desc = d.get("description") or {}
|
||||
bi = d.get("build_information") or {}
|
||||
return _first_str(
|
||||
d.get("version"),
|
||||
d.get("node_version"),
|
||||
bi.get("build_version"),
|
||||
bi.get("version"),
|
||||
(desc.get("software") or {}).get("version"),
|
||||
(desc.get("build_information") or {}).get("build_version"),
|
||||
)
|
||||
|
||||
|
||||
# history storage in data/data.yaml + 30d cleanup + window helpers
|
||||
|
||||
def load_history(path: str) -> Dict[str, Any]:
|
||||
if not os.path.exists(path):
|
||||
return {}
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
try:
|
||||
return yaml.safe_load(f) or {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def save_history(path: str, data: Dict[str, Any]) -> None:
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
yaml.safe_dump(data, f, sort_keys=True)
|
||||
|
||||
def add_history_point(hist: Dict[str, Any], node_id: int, epoch_ts: float, uptime: float, op_bal: float) -> None:
|
||||
key = str(node_id)
|
||||
lst = hist.setdefault(key, [])
|
||||
lst.append({
|
||||
"ts": epoch_ts,
|
||||
"uptime": round(float(uptime), 6),
|
||||
"operator_balance": round(float(op_bal), 6),
|
||||
})
|
||||
|
||||
def last_snapshot(hist: Dict[str, Any], node_id: int) -> Optional[Dict[str, Any]]:
|
||||
key = str(node_id)
|
||||
if key not in hist:
|
||||
return None
|
||||
lst = hist[key]
|
||||
if not lst:
|
||||
return None
|
||||
return sorted(lst, key=lambda x: x.get("ts", 0.0))[-1]
|
||||
|
||||
def cleanup_history_older_than(hist: Dict[str, Any], cutoff_ts: float) -> None:
|
||||
# remove entries older than cutoff - 30 days
|
||||
for key, lst in list(hist.items()):
|
||||
new_lst = [e for e in lst if to_float(e.get("ts"), 0.0) >= cutoff_ts]
|
||||
if new_lst:
|
||||
hist[key] = new_lst
|
||||
else:
|
||||
del hist[key]
|
||||
|
||||
def scaled_window_change(
|
||||
hist: Dict[str, Any],
|
||||
node_id: int,
|
||||
now: float,
|
||||
window_days: float,
|
||||
current_balance: float
|
||||
) -> Optional[Tuple[float, float, float]]:
|
||||
key = str(node_id)
|
||||
if key not in hist or not hist[key]:
|
||||
return None
|
||||
cutoff_ts = now - window_days * 24 * 3600
|
||||
candidates = [e for e in hist[key] if to_float(e.get("ts"), 0.0) <= cutoff_ts]
|
||||
if not candidates:
|
||||
return None
|
||||
snap = sorted(candidates, key=lambda x: x.get("ts", 0.0))[-1]
|
||||
span_hours = max(0.0, (now - to_float(snap.get("ts"), now)) / 3600.0)
|
||||
if span_hours <= 0:
|
||||
return None
|
||||
profit_raw = current_balance - to_float(snap.get("operator_balance"), 0.0)
|
||||
target_hours = window_days * 24.0
|
||||
profit_scaled = profit_raw * (target_hours / span_hours)
|
||||
hourly_scaled = profit_scaled / target_hours
|
||||
return profit_scaled, span_hours, hourly_scaled
|
||||
|
||||
|
||||
# output coloring fns
|
||||
|
||||
def colorize(text: str, color_name: str) -> str:
|
||||
mapping = {
|
||||
"green": Fore.GREEN,
|
||||
"yellow": Fore.YELLOW,
|
||||
"orange": Fore.MAGENTA,
|
||||
"red": Fore.RED,
|
||||
}
|
||||
c = mapping.get(color_name, "")
|
||||
if not c:
|
||||
return text
|
||||
return f"{c}{text}{Style.RESET_ALL}"
|
||||
|
||||
def uptime_color_name(u: float) -> str:
|
||||
if u >= 0.95:
|
||||
return "green"
|
||||
if u >= 0.90:
|
||||
return "yellow"
|
||||
if u >= 0.80:
|
||||
return "orange"
|
||||
return "red"
|
||||
|
||||
|
||||
# main program body
|
||||
|
||||
def main() -> None:
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
wallets = read_wallets_csv(ADDR_CSV)
|
||||
if not wallets:
|
||||
log(f"{Fore.RED}No wallets found in {ADDR_CSV}{Style.RESET_ALL}")
|
||||
sys.exit(1)
|
||||
log(f"Found {len(wallets)} wallet(s) in {ADDR_CSV}")
|
||||
|
||||
# preserve input order per wallet for per-tag tables
|
||||
wallet_order = {addr: idx for idx, (addr, _tag) in enumerate(wallets)}
|
||||
|
||||
nodes = fetch_nodes_all()
|
||||
bonded = fetch_bonded_all()
|
||||
described = fetch_described_all()
|
||||
|
||||
# indexes
|
||||
idx_nodes_by_wallet: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
||||
for n in nodes:
|
||||
w = (n.get("bonding_address") or "").strip()
|
||||
if w:
|
||||
idx_nodes_by_wallet[w].append(n)
|
||||
|
||||
idx_desc_by_node_id: Dict[int, Dict[str, Any]] = {}
|
||||
for d in described:
|
||||
nid = to_int(d.get("node_id"), 0)
|
||||
if nid:
|
||||
idx_desc_by_node_id[nid] = d
|
||||
|
||||
idx_bonded_by_owner: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
||||
for b in bonded:
|
||||
bi = b.get("bond_information", {})
|
||||
owner = (bi.get("owner") or "").strip()
|
||||
if owner:
|
||||
idx_bonded_by_owner[owner].append(b)
|
||||
|
||||
headers_csv = [
|
||||
"node_id",
|
||||
"hostname",
|
||||
"identity_key",
|
||||
"wallet",
|
||||
"uptime",
|
||||
"version",
|
||||
"operator_balance",
|
||||
"profit_difference",
|
||||
"epochs",
|
||||
"average_hour",
|
||||
"7_days",
|
||||
"7_days_average",
|
||||
"30_days",
|
||||
"30_days_average",
|
||||
"tag",
|
||||
]
|
||||
|
||||
hist = load_history(HIST_FILE)
|
||||
now = now_ts()
|
||||
|
||||
out_rows: List[Dict[str, Any]] = []
|
||||
rows_by_tag: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
||||
|
||||
THIRTY_DAYS_SEC = 30 * 24 * 3600
|
||||
cleanup_history_older_than(hist, now - THIRTY_DAYS_SEC) # prune before use
|
||||
|
||||
for wallet_addr, tag in wallets:
|
||||
wallet_nodes = idx_nodes_by_wallet.get(wallet_addr, [])
|
||||
|
||||
if not wallet_nodes:
|
||||
# fallback via bonded -> described + balance
|
||||
for b in idx_bonded_by_owner.get(wallet_addr, []):
|
||||
bi = b.get("bond_information", {})
|
||||
nid = to_int(bi.get("node_id"), 0)
|
||||
if nid <= 0:
|
||||
continue
|
||||
d = idx_desc_by_node_id.get(nid, {})
|
||||
desc = d.get("description", {}) if isinstance(d, dict) else {}
|
||||
hostinfo = desc.get("host_information", {}) if isinstance(desc, dict) else {}
|
||||
hostname = hostinfo.get("hostname") or ""
|
||||
node = bi.get("node", {}) if isinstance(bi, dict) else {}
|
||||
identity_key = node.get("identity_key") or ""
|
||||
|
||||
op_bal = fetch_balance_total_nym(wallet_addr)
|
||||
uptime = 0.0
|
||||
|
||||
# since last time change calculation
|
||||
prev = last_snapshot(hist, nid)
|
||||
prev_bal = to_float(prev.get("operator_balance"), 0.0) if prev else None
|
||||
prev_ts = to_float(prev.get("ts"), 0.0) if prev else None
|
||||
diff = 0.0
|
||||
hours = 0.0
|
||||
if prev is not None:
|
||||
diff = op_bal - prev_bal
|
||||
hours = max(0.0, (now - prev_ts) / 3600.0)
|
||||
|
||||
# last 7 / 30 days calculation
|
||||
seven = scaled_window_change(hist, nid, now, 7.0, op_bal)
|
||||
thirty = scaled_window_change(hist, nid, now, 30.0, op_bal)
|
||||
|
||||
row = {
|
||||
"node_id": nid,
|
||||
"hostname": hostname,
|
||||
"identity_key": identity_key,
|
||||
"wallet": wallet_addr,
|
||||
"uptime": uptime,
|
||||
"version": extract_version_from_desc(d),
|
||||
"operator_balance": op_bal,
|
||||
"profit_difference": diff,
|
||||
"epochs": hours,
|
||||
"average_hour": (diff / hours) if hours > 0 else 0.0,
|
||||
"7_days": f"{seven[0]:.6f}" if seven else "no 7 days data stored",
|
||||
"7_days_average": f"{seven[2]:.6f}" if seven else "no 7 days data stored",
|
||||
"30_days": f"{thirty[0]:.6f}" if thirty else "no 30 days data stored",
|
||||
"30_days_average": f"{thirty[2]:.6f}" if thirty else "no 30 days data stored",
|
||||
"tag": tag,
|
||||
"_prev_balance": prev_bal,
|
||||
"_prev_ts": prev_ts,
|
||||
"_wallet_order": wallet_order.get(wallet_addr, 10**9),
|
||||
}
|
||||
|
||||
# append current snapshot & prune >30d
|
||||
add_history_point(hist, nid, now, uptime, op_bal)
|
||||
|
||||
out_rows.append(row)
|
||||
rows_by_tag[tag].append(row)
|
||||
continue
|
||||
|
||||
# path from /nodes
|
||||
for n in wallet_nodes:
|
||||
nid = to_int(n.get("node_id"), 0)
|
||||
if nid <= 0:
|
||||
continue
|
||||
identity_key = n.get("identity_key") or ""
|
||||
uptime = to_float(n.get("uptime"), 0.0)
|
||||
desc = n.get("description") or {}
|
||||
hostinfo = desc.get("host_information") or {}
|
||||
hostname = hostinfo.get("hostname") or ""
|
||||
op_unym = to_float(n.get("rewarding_details", {}).get("operator"), 0.0)
|
||||
op_bal = op_unym / 1_000_000.0
|
||||
if op_bal <= 0:
|
||||
op_bal = fetch_balance_total_nym(wallet_addr)
|
||||
|
||||
prev = last_snapshot(hist, nid)
|
||||
prev_bal = to_float(prev.get("operator_balance"), 0.0) if prev else None
|
||||
prev_ts = to_float(prev.get("ts"), 0.0) if prev else None
|
||||
|
||||
diff = 0.0
|
||||
hours = 0.0
|
||||
if prev is not None:
|
||||
diff = op_bal - prev_bal
|
||||
hours = max(0.0, (now - prev_ts) / 3600.0)
|
||||
|
||||
seven = scaled_window_change(hist, nid, now, 7.0, op_bal)
|
||||
thirty = scaled_window_change(hist, nid, now, 30.0, op_bal)
|
||||
|
||||
row = {
|
||||
"node_id": nid,
|
||||
"hostname": hostname,
|
||||
"identity_key": identity_key,
|
||||
"wallet": wallet_addr,
|
||||
"uptime": uptime,
|
||||
"version": extract_version_from_node(n),
|
||||
"operator_balance": op_bal,
|
||||
"profit_difference": diff,
|
||||
"epochs": hours,
|
||||
"average_hour": (diff / hours) if hours > 0 else 0.0,
|
||||
"7_days": f"{seven[0]:.6f}" if seven else "no 7 days data stored",
|
||||
"7_days_average": f"{seven[2]:.6f}" if seven else "no 7 days data stored",
|
||||
"30_days": f"{thirty[0]:.6f}" if thirty else "no 30 days data stored",
|
||||
"30_days_average": f"{thirty[2]:.6f}" if thirty else "no 30 days data stored",
|
||||
"tag": tag,
|
||||
"_prev_balance": prev_bal,
|
||||
"_prev_ts": prev_ts,
|
||||
"_wallet_order": wallet_order.get(wallet_addr, 10**9),
|
||||
}
|
||||
|
||||
add_history_point(hist, nid, now, uptime, op_bal)
|
||||
|
||||
out_rows.append(row)
|
||||
rows_by_tag[tag].append(row)
|
||||
|
||||
# final prune & save
|
||||
cleanup_history_older_than(hist, now - THIRTY_DAYS_SEC)
|
||||
save_history(HIST_FILE, hist)
|
||||
|
||||
# write CSV
|
||||
headers_csv = [
|
||||
"node_id","hostname","identity_key","wallet","uptime","version","operator_balance",
|
||||
"profit_difference","epochs","average_hour","7_days","7_days_average",
|
||||
"30_days","30_days_average","tag",
|
||||
]
|
||||
with open(OUT_CSV, "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=headers_csv)
|
||||
writer.writeheader()
|
||||
for r in out_rows:
|
||||
writer.writerow({
|
||||
"node_id": r.get("node_id",""),
|
||||
"hostname": r.get("hostname",""),
|
||||
"identity_key": r.get("identity_key",""),
|
||||
"wallet": r.get("wallet",""),
|
||||
"uptime": f"{to_float(r.get('uptime'),0.0):.6f}",
|
||||
"version": r.get("version",""),
|
||||
"operator_balance": f"{to_float(r.get('operator_balance'),0.0):.6f}",
|
||||
"profit_difference": f"{to_float(r.get('profit_difference'),0.0):.6f}",
|
||||
"epochs": f"{to_float(r.get('epochs'),0.0):.6f}",
|
||||
"average_hour": f"{to_float(r.get('average_hour'),0.0):.6f}",
|
||||
"7_days": r.get("7_days",""),
|
||||
"7_days_average": r.get("7_days_average",""),
|
||||
"30_days": r.get("30_days",""),
|
||||
"30_days_average": r.get("30_days_average",""),
|
||||
"tag": r.get("tag",""),
|
||||
})
|
||||
|
||||
if not out_rows:
|
||||
log(f"{Fore.YELLOW}No rows produced — check inputs and endpoints.{Style.RESET_ALL}")
|
||||
return
|
||||
|
||||
# per-tag output (preserve input order)
|
||||
for tag, rows in sorted(rows_by_tag.items(), key=lambda kv: kv[0] or ""):
|
||||
headers_print = [
|
||||
"node_id","hostname","wallet","uptime","version",
|
||||
"operator_balance","profit_difference","epochs","average_hour",
|
||||
"7_days","7_days_average","30_days","30_days_average",
|
||||
]
|
||||
view: List[List[str]] = []
|
||||
for r in rows:
|
||||
u = to_float(r.get("uptime"), 0.0)
|
||||
u_col = uptime_color_name(u)
|
||||
view.append([
|
||||
r.get("node_id") or "",
|
||||
(r.get("hostname") or "")[:40],
|
||||
r.get("wallet") or "",
|
||||
colorize(f"{u:.2f}", u_col) if u_col else f"{u:.2f}",
|
||||
r.get("version") or "",
|
||||
f"{to_float(r.get('operator_balance'), 0.0):.2f}",
|
||||
f"{to_float(r.get('profit_difference'), 0.0):.2f}",
|
||||
f"{to_float(r.get('epochs'), 0.0):.2f}",
|
||||
f"{to_float(r.get('average_hour'), 0.0):.2f}",
|
||||
r.get("7_days"),
|
||||
r.get("7_days_average"),
|
||||
r.get("30_days"),
|
||||
r.get("30_days_average"),
|
||||
])
|
||||
|
||||
title = f"Tag: {tag or '(untagged)'} — {len(rows)} node(s)"
|
||||
print("\n" + title)
|
||||
print(tabulate(view, headers=headers_print, tablefmt="github", stralign="right", disable_numparse=True))
|
||||
|
||||
# per-tag summary
|
||||
tag_total_now = sum(to_float(r.get("operator_balance"), 0.0) for r in rows)
|
||||
|
||||
# since last time: sum diffs, average hours across nodes that existed
|
||||
prev_sum = 0.0
|
||||
prev_hours: List[float] = []
|
||||
for r in rows:
|
||||
prev_bal = r.get("_prev_balance")
|
||||
prev_ts = r.get("_prev_ts")
|
||||
if prev_bal is not None and prev_ts is not None:
|
||||
prev_sum += to_float(prev_bal, 0.0)
|
||||
prev_hours.append(max(0.0, (now - to_float(prev_ts, now)) / 3600.0))
|
||||
diff_total = tag_total_now - prev_sum if prev_hours else 0.0
|
||||
hours_since = (sum(prev_hours) / len(prev_hours)) if prev_hours else 0.0
|
||||
hourly = (diff_total / hours_since) if hours_since > 0 else 0.0
|
||||
|
||||
# 7-day / 30-day per-tag totals: sum nodes with data; hours fixed windows if any
|
||||
def _num_or_none(v):
|
||||
try:
|
||||
return float(v)
|
||||
except Exception:
|
||||
return None
|
||||
seven_vals = [_num_or_none(r.get("7_days")) for r in rows]
|
||||
seven_vals = [v for v in seven_vals if v is not None]
|
||||
total7 = sum(seven_vals) if seven_vals else 0.0
|
||||
hours7 = 7.0 * 24.0 if seven_vals else 0.0
|
||||
hourly7 = (total7 / hours7) if hours7 > 0 else 0.0
|
||||
|
||||
thirty_vals = [_num_or_none(r.get("30_days")) for r in rows]
|
||||
thirty_vals = [v for v in thirty_vals if v is not None]
|
||||
total30 = sum(thirty_vals) if thirty_vals else 0.0
|
||||
hours30 = 30.0 * 24.0 if thirty_vals else 0.0
|
||||
hourly30 = (total30 / hours30) if hours30 > 0 else 0.0
|
||||
|
||||
# print output with colored numbers
|
||||
print(
|
||||
"\n"
|
||||
f"Total balance across all wallets: {Style.BRIGHT}{Fore.GREEN}{tag_total_now:.2f}{Style.RESET_ALL} NYM\n"
|
||||
f"Difference of total balance from last time: {Fore.CYAN}{diff_total:.2f}{Style.RESET_ALL} NYM\n"
|
||||
f"Time since last time: {Fore.BLUE}{hours_since:.2f}{Style.RESET_ALL} hours\n"
|
||||
f"Approx hourly difference: {Style.BRIGHT}{Fore.GREEN}{hourly:.2f}{Style.RESET_ALL} NYM/h\n"
|
||||
f"7-day change: "
|
||||
f"{('no 7 days data stored' if not seven_vals else f'{Fore.CYAN}{total7:.2f}{Style.RESET_ALL} NYM, hours: {Fore.BLUE}{hours7:.2f}{Style.RESET_ALL}, hourly: {Style.BRIGHT}{Fore.GREEN}{hourly7:.2f}{Style.RESET_ALL} NYM/h')}\n"
|
||||
f"30-day change: "
|
||||
f"{('no 30 days data stored' if not thirty_vals else f'{Fore.CYAN}{total30:.2f}{Style.RESET_ALL} NYM, hours: {Fore.BLUE}{hours30:.2f}{Style.RESET_ALL}, hourly: {Style.BRIGHT}{Fore.GREEN}{hourly30:.2f}{Style.RESET_ALL} NYM/h')}\n"
|
||||
)
|
||||
|
||||
log(f"\nCSV written to: {OUT_CSV}")
|
||||
log(f"History saved to: {HIST_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted.", file=sys.stderr)
|
||||
sys.exit(130)
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
API_URL="${API_URL:-https://validator.nymtech.net/api/v1/nym-nodes/described}"
|
||||
API_URL="${API_URL:-https://validator.nymtech.net/api/v1/gateways/described}"
|
||||
CONCURRENCY="${CONCURRENCY:-64}" # how many pings in flight
|
||||
PING_TIMEOUT="${PING_TIMEOUT:-7}" # seconds to wait for a single echo reply
|
||||
PING_RETRIES="${PING_RETRIES:-1}" # additional attempts after the first failure
|
||||
@@ -28,13 +28,19 @@ curl -fsSL --retry 3 --retry-delay 1 --compressed "$API_URL" -o "$tmp_json"
|
||||
# extract IPs
|
||||
ip_list="$(mktemp)"
|
||||
jq -r '
|
||||
.data[]?
|
||||
| (.description.host_information.ip_address? // [])[]
|
||||
.[]? as $g
|
||||
| (
|
||||
($g.self_described.host_information.ip_address? // [])
|
||||
+ (
|
||||
[ $g.bond.gateway.host ]
|
||||
| map(select(type=="string"))
|
||||
)
|
||||
)[]
|
||||
' "$tmp_json" \
|
||||
| awk '
|
||||
# very permissive IPv4/IPv6 syntax filters (we let ping validate the rest)
|
||||
function is_ipv4(s){ return (s ~ /^[0-9]{1,3}(\.[0-9]{1,3}){3}$/) }
|
||||
function is_ipv6(s){ return (index(s,":") > 0) }
|
||||
function is_ipv6(s){ return (index(s,":")>0) }
|
||||
{ if(is_ipv4($0) || is_ipv6($0)) print $0 }
|
||||
' \
|
||||
| sort -u > "$ip_list"
|
||||
@@ -93,4 +99,3 @@ awk '{printf "%s,%s\n",$1,$2}' "$num_list" \
|
||||
echo "Done. Results:"
|
||||
echo " $(($(wc -l < "$OK_CSV") - 1)) reachable -> $OK_CSV"
|
||||
echo " $(($(wc -l < "$BAD_CSV") - 1)) not reachable -> $BAD_CSV"
|
||||
|
||||
|
||||
@@ -45,15 +45,21 @@ pub use native_client::MixnetClient;
|
||||
pub use native_client::MixnetClientSender;
|
||||
#[allow(deprecated)]
|
||||
pub use nym_client_core::client::{
|
||||
base_client::storage::{
|
||||
gateways_storage::{ActiveGateway, BadGateway, GatewayRegistration, GatewaysDetailsStore},
|
||||
Ephemeral, MixnetClientStorage, OnDiskPersistent,
|
||||
base_client::{
|
||||
storage::{
|
||||
gateways_storage::{
|
||||
ActiveGateway, BadGateway, GatewayRegistration, GatewaysDetailsStore,
|
||||
},
|
||||
Ephemeral, MixnetClientStorage, OnDiskPersistent,
|
||||
},
|
||||
EventReceiver, EventSender, MixnetClientEvent,
|
||||
},
|
||||
inbound_messages::InputMessage,
|
||||
key_manager::{
|
||||
persistence::{InMemEphemeralKeys, KeyStore, OnDiskKeys},
|
||||
ClientKeys,
|
||||
},
|
||||
mix_traffic::MixTrafficEvent,
|
||||
replies::reply_storage::{
|
||||
fs_backend::Backend as ReplyStorage, CombinedReplyStorage, Empty as EmptyReplyStorage,
|
||||
ReplyStorageBackend,
|
||||
@@ -63,7 +69,7 @@ pub use nym_credential_storage::{
|
||||
ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage,
|
||||
models::StoredIssuedTicketbook, storage::Storage as CredentialStorage,
|
||||
};
|
||||
pub use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
pub use nym_crypto::asymmetric::ed25519;
|
||||
pub use nym_network_defaults::NymNetworkDetails;
|
||||
pub use nym_socks5_client_core::config::Socks5;
|
||||
pub use nym_sphinx::{
|
||||
|
||||
@@ -16,7 +16,7 @@ use nym_client_core::client::base_client::storage::helpers::{
|
||||
use nym_client_core::client::base_client::storage::{
|
||||
Ephemeral, GatewaysDetailsStore, MixnetClientStorage, OnDiskPersistent,
|
||||
};
|
||||
use nym_client_core::client::base_client::BaseClient;
|
||||
use nym_client_core::client::base_client::{BaseClient, EventSender};
|
||||
use nym_client_core::client::key_manager::persistence::KeyStore;
|
||||
use nym_client_core::client::{
|
||||
base_client::BaseClientBuilder, replies::reply_storage::ReplyStorageBackend,
|
||||
@@ -53,6 +53,7 @@ pub struct MixnetClientBuilder<S: MixnetClientStorage = Ephemeral> {
|
||||
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
|
||||
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
|
||||
custom_shutdown: Option<ShutdownTracker>,
|
||||
event_tx: Option<EventSender>,
|
||||
force_tls: bool,
|
||||
user_agent: Option<UserAgent>,
|
||||
#[cfg(unix)]
|
||||
@@ -96,6 +97,7 @@ impl MixnetClientBuilder<OnDiskPersistent> {
|
||||
.await?,
|
||||
gateway_endpoint_config_path: None,
|
||||
custom_shutdown: None,
|
||||
event_tx: None,
|
||||
custom_gateway_transceiver: None,
|
||||
force_tls: false,
|
||||
user_agent: None,
|
||||
@@ -129,6 +131,7 @@ where
|
||||
custom_topology_provider: None,
|
||||
custom_gateway_transceiver: None,
|
||||
custom_shutdown: None,
|
||||
event_tx: None,
|
||||
force_tls: false,
|
||||
user_agent: None,
|
||||
#[cfg(unix)]
|
||||
@@ -152,6 +155,7 @@ where
|
||||
custom_topology_provider: self.custom_topology_provider,
|
||||
custom_gateway_transceiver: self.custom_gateway_transceiver,
|
||||
custom_shutdown: self.custom_shutdown,
|
||||
event_tx: self.event_tx,
|
||||
force_tls: self.force_tls,
|
||||
user_agent: self.user_agent,
|
||||
#[cfg(unix)]
|
||||
@@ -269,6 +273,13 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Use an externally managed shutdown mechanism.
|
||||
#[must_use]
|
||||
pub fn event_tx(mut self, event_tx: EventSender) -> Self {
|
||||
self.event_tx = Some(event_tx);
|
||||
self
|
||||
}
|
||||
|
||||
/// Attempt to wait for the selected gateway (if applicable) to come online if its currently not bonded.
|
||||
#[must_use]
|
||||
pub fn with_wait_for_gateway(mut self, wait_for_gateway: bool) -> Self {
|
||||
@@ -317,8 +328,12 @@ where
|
||||
|
||||
/// Construct a [`DisconnectedMixnetClient`] from the setup specified.
|
||||
pub fn build(self) -> Result<DisconnectedMixnetClient<S>> {
|
||||
let mut client =
|
||||
DisconnectedMixnetClient::new(self.config, self.socks5_config, self.storage)?;
|
||||
let mut client = DisconnectedMixnetClient::new(
|
||||
self.config,
|
||||
self.socks5_config,
|
||||
self.storage,
|
||||
self.event_tx,
|
||||
)?;
|
||||
|
||||
client.custom_gateway_transceiver = self.custom_gateway_transceiver;
|
||||
client.custom_topology_provider = self.custom_topology_provider;
|
||||
@@ -380,6 +395,9 @@ where
|
||||
/// Allows passing an externally controlled shutdown handle.
|
||||
custom_shutdown: Option<ShutdownTracker>,
|
||||
|
||||
/// Sender of mixnet client events to the SDK caller
|
||||
event_tx: Option<EventSender>,
|
||||
|
||||
user_agent: Option<UserAgent>,
|
||||
|
||||
/// Callback on the websocket fd as soon as the connection has been established
|
||||
@@ -415,6 +433,7 @@ where
|
||||
config: Config,
|
||||
socks5_config: Option<Socks5>,
|
||||
storage: S,
|
||||
event_tx: Option<EventSender>,
|
||||
) -> Result<DisconnectedMixnetClient<S>> {
|
||||
// don't create dkg client for the bandwidth controller if credentials are disabled
|
||||
let dkg_query_client = if config.enabled_credentials_mode {
|
||||
@@ -443,6 +462,7 @@ where
|
||||
wait_for_gateway: false,
|
||||
force_tls: false,
|
||||
custom_shutdown: None,
|
||||
event_tx,
|
||||
user_agent: None,
|
||||
#[cfg(unix)]
|
||||
connection_fd_callback: None,
|
||||
@@ -699,6 +719,9 @@ where
|
||||
}
|
||||
};
|
||||
base_builder = base_builder.with_shutdown(shutdown_tracker);
|
||||
if let Some(event_tx) = self.event_tx {
|
||||
base_builder = base_builder.with_event_tx(event_tx);
|
||||
}
|
||||
|
||||
if let Some(gateway_transceiver) = self.custom_gateway_transceiver {
|
||||
base_builder = base_builder.with_gateway_transceiver(gateway_transceiver);
|
||||
|
||||
Reference in New Issue
Block a user