feat: localnet v2 (#6277)

* squashing localnet-v2 commits (again)

cargo fmt

fixes to localnet purge

provide path in the error message

output args

log failed exec

print based on tty

check-prerequisites cmd

checked iptables update

basic kernel features check

enable ipv6 rules

add forwarding rules

squashing localnet-v2 commits

additional changes

propagate custom-dns flag to all run containers

remove is_mock from EcashManager

another localnet squash

unused import

chore: remove redundant testnet manager

missing impl

additional linux fixes

command to rebuild container image

wait for at least 2 blocks

additional node startup fixes

added --custom-dns flag to nym node setup

add gateway probe + wait for DKG magic file

fixed localnet down on linux

container ls

re-enable state resync

additional feature locking

macos adjustments

working nyxd startup on linux

wip linux box

wip

separating network inspect betweewn macos and linux

initial linux feature locking

moved all container commands into a single location

finally working initial node performance

squashing orchestrator commits

cleanup

fixed condition for naive rearrangement

added cache of cosmwasm contracts for speed up on subsequent runs

'down' command

refreshing described cache after nodes are bonded

nym nodes setup + wip on nym api refresh

nodes setup WIP

first pass cleanup

placeholder for nym-node setup

bypassing the dkg

further progress on nym-api setup

wip: api setup

up/down/purge placeholders

persisting contract setup data

fix contract upload by forcing amd64 container platform

wip: contracts setup4

wip: contracts setup3

wip: contracts setup2

wip: contracts setup

include network setup

init and spawn nyxd

build nyxd image in dedicated orchestrator

build nyxd image

squashed cherry-picked lp changes

Bits and bobs to make everything work

Title

MacOS setup instructions

Docker/Container localnet

* clippy

* fixes on non-unix targets

---------

Co-authored-by: durch <durch@users.noreply.github.com>
This commit is contained in:
Jędrzej Stuczyński
2026-03-12 14:46:00 +00:00
committed by GitHub
parent 8001fa7f40
commit e2dd8ac743
164 changed files with 9812 additions and 5434 deletions
Generated
+469 -117
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -157,8 +157,8 @@ members = [
"tools/internal/mixnet-connectivity-check",
# "tools/internal/sdk-version-bump",
"tools/internal/ssl-inject",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/localnet-orchestrator",
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
"tools/internal/validator-status-check",
"tools/nym-cli",
"tools/nym-id-cli",
@@ -192,6 +192,7 @@ default-members = [
"service-providers/network-requester",
"tools/nymvisor",
"nym-registration-client",
"tools/internal/localnet-orchestrator"
]
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
@@ -233,6 +234,7 @@ bloomfilter = "3.0.1"
bs58 = "0.5.1"
bytecodec = "0.4.15"
bytes = "1.11.1"
cargo-edit = "0.13.8"
cargo_metadata = "0.19.2"
celes = "2.6.0"
cfg-if = "1.0.0"
+1 -1
View File
@@ -2,7 +2,7 @@
name = "nym-client-core"
version.workspace = true
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
edition = "2024"
rust-version = "1.85"
license.workspace = true
description = "Crate containing core client functionality and configs, used by all other Nym client implentations"
@@ -32,6 +32,7 @@ const DEFAULT_MIN_MIXNODE_PERFORMANCE: u8 = 50;
const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50;
const DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD: Duration = Duration::from_secs(70 * 60); // 70min -> full epoch (1h) + a bit of overhead
const DEFAULT_MAX_STARTUP_TOPOLOGY_WAITING_PERIOD: Duration = Duration::from_secs(70 * 60); // 70min -> full epoch (1h) + a bit of overhead
// Set this to a high value for now, so that we don't risk sporadic timeouts that might cause
// bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the
@@ -555,6 +556,11 @@ pub struct Topology {
#[serde(with = "humantime_serde")]
pub max_startup_gateway_waiting_period: Duration,
/// Defines how long the client is going to wait on startup for minimal topology to become online,
/// before abandoning the procedure.
#[serde(with = "humantime_serde")]
pub max_startup_network_waiting_period: Duration,
/// Specifies a minimum performance of a mixnode that is used on route construction.
/// This setting is only applicable when `NymApi` topology is used.
pub minimum_mixnode_performance: u8,
@@ -583,6 +589,7 @@ impl Default for Topology {
topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT,
disable_refreshing: false,
max_startup_gateway_waiting_period: DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD,
max_startup_network_waiting_period: DEFAULT_MAX_STARTUP_TOPOLOGY_WAITING_PERIOD,
minimum_mixnode_performance: DEFAULT_MIN_MIXNODE_PERFORMANCE,
minimum_gateway_performance: DEFAULT_MIN_GATEWAY_PERFORMANCE,
use_extended_topology: false,
@@ -159,6 +159,7 @@ impl From<ConfigV6> for Config {
use_extended_topology: value.debug.topology.use_extended_topology,
ignore_egress_epoch_role: value.debug.topology.ignore_egress_epoch_role,
ignore_ingress_epoch_role: value.debug.topology.ignore_ingress_epoch_role,
..Default::default()
},
reply_surbs: ReplySurbs {
minimum_reply_surb_storage_threshold: value
@@ -160,7 +160,10 @@ where
)
.await?;
} else {
info!("registered with new gateway {} (under address {address}), but this will not be our default address", gateway_details.gateway_id);
info!(
"registered with new gateway {} (under address {address}), but this will not be our default address",
gateway_details.gateway_id
);
}
Ok(GatewayInfo {
@@ -4,13 +4,13 @@
use super::mix_traffic::ClientRequestSender;
use super::received_buffer::ReceivedBufferMessage;
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::base_client::storage::helpers::store_client_keys;
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::key_manager::persistence::KeyStore;
use crate::client::mix_traffic::transceiver::{GatewayReceiver, GatewayTransceiver, RemoteGateway};
use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController, MixTrafficEvent};
use crate::client::real_messages_control;
@@ -52,12 +52,12 @@ use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_sphinx::receiver::{ReconstructedMessage, SphinxMessageReceiver};
use nym_statistics_common::clients::ClientStatsSender;
use nym_statistics_common::generate_client_stats_id;
use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
use nym_task::ShutdownTracker;
use nym_topology::provider_trait::TopologyProvider;
use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths};
use nym_topology::HardcodedTopologyProvider;
use nym_topology::provider_trait::TopologyProvider;
use nym_validator_client::nym_api::NymApiClientExt;
use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent};
use nym_validator_client::{UserAgent, nyxd::contract_traits::DkgQueryClient};
use rand::prelude::SliceRandom;
use rand::rngs::OsRng;
use rand::thread_rng;
@@ -220,6 +220,7 @@ pub struct BaseClientBuilder<C, S: MixnetClientStorage> {
nym_api_urls: Option<Vec<nym_network_defaults::ApiUrl>>,
wait_for_gateway: bool,
wait_for_initial_topology: bool,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send>>,
shutdown: Option<ShutdownTracker>,
@@ -250,6 +251,7 @@ where
dkg_query_client,
nym_api_urls: None,
wait_for_gateway: false,
wait_for_initial_topology: false,
custom_topology_provider: None,
custom_gateway_transceiver: None,
shutdown: None,
@@ -305,6 +307,12 @@ where
self
}
#[must_use]
pub fn with_wait_for_initial_topology(mut self, wait_for_initial_topology: bool) -> Self {
self.wait_for_initial_topology = wait_for_initial_topology;
self
}
#[must_use]
pub fn with_topology_provider(
mut self,
@@ -674,6 +682,7 @@ where
topology_accessor: TopologyAccessor,
local_gateway: NodeIdentity,
wait_for_gateway: bool,
wait_for_initial_topology: bool,
shutdown_tracker: &ShutdownTracker,
) -> Result<(), ClientCoreError> {
let topology_refresher_config =
@@ -694,6 +703,46 @@ where
tracing::info!("Obtaining initial network topology");
topology_refresher.try_refresh().await;
// 1. wait for the minimum topology (if applicable)
if topology_refresher
.ensure_topology_is_routable()
.await
.is_err()
&& wait_for_initial_topology
{
if let Err(err) = topology_refresher
.wait_for_initial_network(topology_config.max_startup_network_waiting_period)
.await
{
tracing::error!(
"the network did not come become online within the specified timeout: {err}"
);
return Err(err.into());
}
}
// 2. wait for our gateway (if applicable)
if topology_refresher
.ensure_contains_routable_egress(local_gateway)
.await
.is_err()
&& wait_for_gateway
{
if let Err(err) = topology_refresher
.wait_for_gateway(
local_gateway,
topology_config.max_startup_gateway_waiting_period,
)
.await
{
tracing::error!(
"the gateway did not come back online within the specified timeout: {err}"
);
return Err(err.into());
}
}
// 3. check if the topology is routable (in case we were NOT waiting for it)
if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
tracing::error!(
"The current network topology seem to be insufficient to route any packets through \
@@ -702,30 +751,15 @@ where
return Err(ClientCoreError::InsufficientNetworkTopology(err));
}
let gateway_wait_timeout = if wait_for_gateway {
Some(topology_config.max_startup_gateway_waiting_period)
} else {
None
};
// 4. check if the gateway exists (in case we were NOT waiting for it)
if let Err(err) = topology_refresher
.ensure_contains_routable_egress(local_gateway)
.await
{
if let Some(waiting_timeout) = gateway_wait_timeout {
if let Err(err) = topology_refresher
.wait_for_gateway(local_gateway, waiting_timeout)
.await
{
tracing::error!(
"the gateway did not come back online within the specified timeout: {err}"
);
return Err(err.into());
}
} else {
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());
}
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());
}
if !topology_config.disable_refreshing {
@@ -1024,6 +1058,7 @@ where
shared_topology_accessor.clone(),
self_address.gateway(),
self.wait_for_gateway,
self.wait_for_initial_topology,
&shutdown_tracker.clone(),
)
.await?;
@@ -1195,9 +1230,11 @@ mod tests {
]);
assert_eq!(network_details.nym_api_urls.as_ref().unwrap().len(), 2);
assert!(network_details.nym_api_urls.as_ref().unwrap()[1]
.front_hosts
.is_some());
assert!(
network_details.nym_api_urls.as_ref().unwrap()[1]
.front_hosts
.is_some()
);
}
#[test]
@@ -1210,11 +1247,13 @@ mod tests {
assert_eq!(api_url.url, "https://nym-frontdoor.vercel.app/api/");
assert_eq!(api_url.front_hosts.as_ref().unwrap().len(), 2);
assert!(api_url
.front_hosts
.as_ref()
.unwrap()
.contains(&"vercel.app".to_string()));
assert!(
api_url
.front_hosts
.as_ref()
.unwrap()
.contains(&"vercel.app".to_string())
);
}
#[test]
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
client::replies::reply_storage::{CombinedReplyStorage, ReplyStorageBackend, fs_backend},
config,
config::Config,
error::ClientCoreError,
@@ -10,7 +10,7 @@ use crate::{
use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
use nym_validator_client::{QueryHttpRpcNyxdClient, nyxd};
use std::{io, path::Path};
use time::OffsetDateTime;
use tracing::{error, info, trace};
@@ -24,7 +24,9 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
Ok(backend) => backend,
Err(err) => {
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
error!(
"setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}"
);
return Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
});
@@ -93,7 +95,9 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
match fs_backend::Backend::try_load(db_path).await {
Ok(backend) => Ok(backend),
Err(err) => {
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
error!(
"setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future"
);
archive_corrupted_database(db_path).await?;
setup_fresh_backend(db_path, surb_config).await
}
@@ -1,8 +1,8 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::key_manager::persistence::KeyStore;
use crate::client::key_manager::ClientKeys;
use crate::client::key_manager::persistence::KeyStore;
use crate::error::ClientCoreError;
use nym_client_core_gateways_storage::{
ActiveGateway, GatewayPublishedData, GatewayRegistration, GatewaysDetailsStore,
@@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
pub mod v1_1_33 {
use crate::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33;
use crate::config::disk_persistence::CommonClientPaths;
use crate::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33;
use crate::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33;
use crate::error::ClientCoreError;
@@ -11,8 +11,8 @@ use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::cover::generate_loop_cover_packet;
use nym_sphinx::params::{PacketSize, PacketType};
use nym_sphinx::utils::sample_poisson_duration;
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use rand::{rngs::OsRng, CryptoRng, Rng};
use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent};
use rand::{CryptoRng, Rng, rngs::OsRng};
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
@@ -20,10 +20,10 @@ use tokio::sync::mpsc::error::TrySendError;
use tracing::*;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::{sleep, Sleep};
use tokio::time::{Sleep, sleep};
#[cfg(target_arch = "wasm32")]
use wasmtimer::tokio::{sleep, Sleep};
use wasmtimer::tokio::{Sleep, sleep};
pub struct LoopCoverTrafficStream<R>
where
@@ -179,7 +179,9 @@ impl LoopCoverTrafficStream<OsRng> {
) {
Ok(topology) => topology,
Err(err) => {
warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}");
warn!(
"We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}"
);
return;
}
};
@@ -13,10 +13,10 @@ use crate::config::disk_persistence::ClientKeysPaths;
#[cfg(not(target_arch = "wasm32"))]
use nym_crypto::asymmetric::{ed25519, x25519};
#[cfg(not(target_arch = "wasm32"))]
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
#[cfg(not(target_arch = "wasm32"))]
use nym_pemstore::KeyPairPath;
#[cfg(not(target_arch = "wasm32"))]
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
#[cfg(not(target_arch = "wasm32"))]
use nym_sphinx::acknowledgements::AckKey;
// we have to define it as an async trait since wasm storage is async
@@ -4,8 +4,8 @@
use async_trait::async_trait;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_crypto::asymmetric::ed25519;
use nym_gateway_client::error::GatewayClientError;
use nym_gateway_client::GatewayClient;
use nym_gateway_client::error::GatewayClientError;
pub use nym_gateway_client::{GatewayPacketRouter, PacketRouter};
use nym_gateway_requests::ClientRequest;
use nym_sphinx::forwarding::packet::MixPacket;
@@ -2,13 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
use super::action_controller::{AckActionSender, Action};
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent};
use futures::StreamExt;
use nym_gateway_client::AcknowledgementReceiver;
use nym_sphinx::{
acknowledgements::{identifier::recover_identifier, AckKey},
chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID},
acknowledgements::{AckKey, identifier::recover_identifier},
chunking::fragment::{COVER_FRAG_ID, FragmentIdentifier},
};
use nym_task::ShutdownToken;
use std::sync::Arc;
@@ -3,11 +3,11 @@
use super::PendingAcknowledgement;
use crate::client::real_messages_control::acknowledgement_control::RetransmissionRequestSender;
use futures::channel::mpsc;
use futures::StreamExt;
use futures::channel::mpsc;
use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey};
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_sphinx::Delay as SphinxDelay;
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_task::ShutdownToken;
use std::collections::HashMap;
use std::sync::Arc;
@@ -9,8 +9,8 @@ use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::params::PacketType;
use nym_task::connections::TransmissionLane;
use nym_task::ShutdownToken;
use nym_task::connections::TransmissionLane;
use rand::{CryptoRng, Rng};
use tracing::*;
@@ -16,10 +16,10 @@ use nym_gateway_client::AcknowledgementReceiver;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::params::{PacketSize, PacketType};
use nym_sphinx::{
Delay as SphinxDelay,
acknowledgements::AckKey,
addressing::clients::Recipient,
chunking::fragment::{Fragment, FragmentIdentifier},
Delay as SphinxDelay,
};
use nym_statistics_common::clients::ClientStatsSender;
use rand::{CryptoRng, Rng};
@@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use super::{
action_controller::{AckActionSender, Action},
PendingAcknowledgement, RetransmissionRequestReceiver,
action_controller::{AckActionSender, Action},
};
use crate::client::real_messages_control::acknowledgement_control::PacketDestination;
use crate::client::real_messages_control::message_handler::{MessageHandler, PreparationError};
@@ -13,7 +13,7 @@ use futures::StreamExt;
use nym_sphinx::chunking::fragment::Fragment;
use nym_sphinx::preparer::PreparedFragment;
use nym_sphinx::{addressing::clients::Recipient, params::PacketType};
use nym_task::{connections::TransmissionLane, ShutdownToken};
use nym_task::{ShutdownToken, connections::TransmissionLane};
use rand::{CryptoRng, Rng};
use std::sync::{Arc, Weak};
use tracing::*;
@@ -1,10 +1,10 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::action_controller::{AckActionSender, Action};
use super::SentPacketNotificationReceiver;
use super::action_controller::{AckActionSender, Action};
use futures::StreamExt;
use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID};
use nym_sphinx::chunking::fragment::{COVER_FRAG_ID, FragmentIdentifier};
use tracing::*;
/// Module responsible for starting up retransmission timers.
@@ -10,17 +10,17 @@ use crate::client::replies::reply_controller::MaxRetransmissions;
use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags};
use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit};
use nym_client_core_surb_storage::RetrievedReplySurb;
use nym_sphinx::Delay;
use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier};
use nym_sphinx::message::NymMessage;
use nym_sphinx::params::{PacketSize, PacketType};
use nym_sphinx::preparer::{MessagePreparer, PreparedFragment};
use nym_sphinx::Delay;
use nym_task::connections::TransmissionLane;
use nym_task::ShutdownToken;
use nym_task::connections::TransmissionLane;
use nym_topology::{NymRouteProvider, NymTopologyError};
use rand::{CryptoRng, Rng};
use std::collections::HashMap;
@@ -272,7 +272,9 @@ where
let primary_count = msg.required_packets(self.config.primary_packet_size);
let secondary_count = msg.required_packets(secondary_packet);
trace!("This message would require: {primary_count} primary packets or {secondary_count} secondary packets...");
trace!(
"This message would require: {primary_count} primary packets or {secondary_count} secondary packets..."
);
// if there would be no benefit in using the secondary packet - use the primary (duh)
if primary_count <= secondary_count {
trace!("so choosing primary for this message");
@@ -25,9 +25,9 @@ use nym_gateway_client::AcknowledgementReceiver;
use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient;
use nym_statistics_common::clients::ClientStatsSender;
use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths};
use nym_task::ShutdownToken;
use rand::{rngs::OsRng, CryptoRng, Rng};
use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths};
use rand::{CryptoRng, Rng, rngs::OsRng};
use std::sync::Arc;
use crate::client::replies::reply_controller::key_rotation_helpers::KeyRotationConfig;
@@ -17,11 +17,11 @@ use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::params::PacketSize;
use nym_sphinx::preparer::PreparedFragment;
use nym_sphinx::utils::sample_poisson_duration;
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent};
use nym_task::ShutdownToken;
use nym_task::connections::{
ConnectionCommand, ConnectionCommandReceiver, ConnectionId, LaneQueueLengths, TransmissionLane,
};
use nym_task::ShutdownToken;
use rand::{CryptoRng, Rng};
use std::pin::Pin;
use std::sync::Arc;
@@ -29,11 +29,11 @@ use std::time::Duration;
use tracing::*;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::{sleep, Sleep};
use tokio::time::{Sleep, sleep};
// use nym_wasm_utils::console_log;
#[cfg(target_arch = "wasm32")]
use wasmtimer::tokio::{sleep, Sleep};
use wasmtimer::tokio::{Sleep, sleep};
mod sending_delay_controller;
/// Configurable parameters of the `OutQueueControl`
@@ -230,7 +230,9 @@ where
let (next_message, fragment_id, packet_size) = match next_message {
StreamMessage::Cover => {
let cover_traffic_packet_size = self.loop_cover_message_size();
trace!("the next loop cover message will be put in a {cover_traffic_packet_size} packet");
trace!(
"the next loop cover message will be put in a {cover_traffic_packet_size} packet"
);
// TODO for way down the line: in very rare cases (during topology update) we might have
// to wait a really tiny bit before actually obtaining the permit hence messing with our
@@ -244,7 +246,9 @@ where
) {
Ok(topology) => topology,
Err(err) => {
warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}");
warn!(
"We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}"
);
return;
}
};
@@ -436,7 +440,7 @@ where
}
}
if let Some(ref mut next_delay) = &mut self.next_delay {
if let Some(next_delay) = &mut self.next_delay {
// it is not yet time to return a message
if next_delay.as_mut().poll(cx).is_pending() {
return Poll::Pending;
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::helpers::{get_time_now, Instant};
use crate::client::helpers::{Instant, get_time_now};
use std::time::Duration;
// The minimum time between increasing the average delay between packets. If we hit the ceiling in
@@ -5,20 +5,20 @@ use crate::client::helpers::get_time_now;
use crate::client::replies::{
reply_controller::ReplyControllerSender, reply_storage::SentReplyKeys,
};
use futures::StreamExt;
use futures::channel::mpsc;
use futures::lock::Mutex;
use futures::StreamExt;
use nym_crypto::asymmetric::x25519;
use nym_crypto::Digest;
use nym_crypto::asymmetric::x25519;
use nym_gateway_client::MixnetMessageReceiver;
use nym_sphinx::anonymous_replies::requests::{
RepliableMessage, RepliableMessageContent, ReplyMessage, ReplyMessageContent,
};
use nym_sphinx::anonymous_replies::{encryption_key::EncryptionKeyDigest, SurbEncryptionKey};
use nym_sphinx::anonymous_replies::{SurbEncryptionKey, encryption_key::EncryptionKeyDigest};
use nym_sphinx::message::{NymMessage, PlainMessage};
use nym_sphinx::params::ReplySurbKeyDigestAlgorithm;
use nym_sphinx::receiver::{MessageReceiver, MessageRecoveryError, ReconstructedMessage};
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use nym_statistics_common::clients::{ClientStatsSender, packet_statistics::PacketStatisticsEvent};
use nym_task::ShutdownToken;
use std::collections::HashSet;
use std::sync::Arc;
@@ -78,14 +78,19 @@ impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
let fragment = match self.message_receiver.recover_fragment(fragment_data) {
Err(err) => {
warn!("failed to recover fragment from raw data: {err}. The whole underlying message might be corrupted and unrecoverable!");
warn!(
"failed to recover fragment from raw data: {err}. The whole underlying message might be corrupted and unrecoverable!"
);
return None;
}
Ok(frag) => frag,
};
if self.recently_reconstructed.contains(&fragment.id()) {
debug!("Received a chunk of already re-assembled message ({:?})! It probably got here because the ack got lost", fragment.id());
debug!(
"Received a chunk of already re-assembled message ({:?})! It probably got here because the ack got lost",
fragment.id()
);
return None;
}
@@ -93,7 +98,9 @@ impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
match self.message_receiver.insert_new_fragment(fragment) {
Err(err) => match err {
MessageRecoveryError::MalformedReconstructedMessage { source, used_sets } => {
error!("message reconstruction failed - {source}. Attempting to re-use the message sets...");
error!(
"message reconstruction failed - {source}. Attempting to re-use the message sets..."
);
// TODO: should we really insert reconstructed sets? could this be abused for some attack?
for set_id in used_sets {
if !self.recently_reconstructed.insert(set_id) {
@@ -144,7 +151,9 @@ impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
&mut raw_fragment,
) {
Err(err) => {
warn!("failed to recover fragment data: {err}. The whole underlying message might be corrupted and unrecoverable!");
warn!(
"failed to recover fragment data: {err}. The whole underlying message might be corrupted and unrecoverable!"
);
return None;
}
Ok(frag_data) => frag_data,
@@ -275,7 +284,9 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
}
RepliableMessageContent::Heartbeat(content) => {
let additional_reply_surbs = content.additional_reply_surbs;
error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)");
error!(
"received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)"
);
(additional_reply_surbs, false)
}
RepliableMessageContent::DataV2(content) => {
@@ -304,7 +315,9 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
}
RepliableMessageContent::HeartbeatV2(content) => {
let additional_reply_surbs = content.additional_reply_surbs;
error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)");
error!(
"received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)"
);
(additional_reply_surbs, false)
}
};
@@ -380,7 +393,9 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
if let Some(sender) = &inner_guard.message_sender {
trace!("Sending reconstructed messages to announced sender");
if let Err(err) = sender.unbounded_send(reconstructed_messages) {
warn!("The reconstructed message receiver went offline without explicit notification (relevant error: - {err})");
warn!(
"The reconstructed message receiver went offline without explicit notification (relevant error: - {err})"
);
inner_guard.message_sender = None;
inner_guard.messages.extend(err.into_inner());
}
@@ -5,15 +5,15 @@ use crate::client::real_messages_control::acknowledgement_control::PendingAcknow
use crate::client::real_messages_control::message_handler::{
FragmentWithMaxRetransmissions, MessageHandler, PreparationError,
};
use crate::client::replies::reply_controller::key_rotation_helpers::SurbRefreshState;
use crate::client::replies::reply_controller::Config;
use crate::client::replies::reply_controller::key_rotation_helpers::SurbRefreshState;
use crate::client::topology_control::TopologyAccessor;
use crate::client::transmission_buffer::TransmissionBuffer;
use futures::channel::oneshot;
use nym_client_core_surb_storage::{ReceivedReplySurb, ReceivedReplySurbsMap};
use nym_crypto::aes::cipher::crypto_common::rand_core::CryptoRng;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_task::connections::{ConnectionId, TransmissionLane};
use nym_topology::NymTopologyMetadata;
@@ -50,7 +50,9 @@ impl SenderData {
let pending_retransmissions = self.pending_retransmissions.len();
let total_pending = pending_retransmissions + pending_replies;
debug!("total queue size: {total_pending} = pending data {pending_replies} + pending retransmission {pending_retransmissions}");
debug!(
"total queue size: {total_pending} = pending data {pending_replies} + pending retransmission {pending_retransmissions}"
);
total_pending
}
@@ -200,7 +202,9 @@ where
let total_required_surbs = total_queue + target_surbs_after_clearing_queue;
let total_available_surbs = pending_surbs + available_surbs;
debug!("available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..+{min_surbs_threshold_buffer}..{max_surbs_threshold}");
debug!(
"available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..+{min_surbs_threshold_buffer}..{max_surbs_threshold}"
);
// We should request more surbs if:
// 1. We haven't hit the maximum surb threshold, and
@@ -225,9 +229,13 @@ where
.is_none()
{
// don't report it every single time
warn!("received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!");
warn!(
"received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!"
);
} else {
trace!("received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!");
trace!(
"received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!"
);
}
return;
}
@@ -383,7 +391,9 @@ where
let (surbs_for_reply, _) = self.surbs_storage.get_reply_surbs(&target, to_take.len());
let Some(surbs_for_reply) = surbs_for_reply else {
error!("somehow different task has stolen our reply surbs! - this should have been impossible");
error!(
"somehow different task has stolen our reply surbs! - this should have been impossible"
);
self.re_insert_pending_retransmission(&target, to_take);
return;
};
@@ -459,7 +469,9 @@ where
.get_reply_surbs(&target, to_send_clone.len());
let Some(surbs_for_reply) = surbs_for_reply else {
error!("somehow different task has stolen our reply surbs! - this should have been impossible");
error!(
"somehow different task has stolen our reply surbs! - this should have been impossible"
);
self.re_insert_pending_replies(&target, to_send);
return;
};
@@ -543,7 +555,9 @@ where
let ack_ref = match timed_out_ack.upgrade() {
Some(ack) => ack,
None => {
debug!("we received the ack for one of the reply packets as we were putting it in the retransmission queue");
debug!(
"we received the ack for one of the reply packets as we were putting it in the retransmission queue"
);
return;
}
};
@@ -657,9 +671,13 @@ where
// only log at higher level if it's the first time this error has occurred in a while
if now - last_failure > time::Duration::seconds(30) {
warn!("failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}")
warn!(
"failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}"
)
} else {
debug!("failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}")
debug!(
"failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}"
)
}
}
}
@@ -681,7 +699,10 @@ where
.surbs_storage
.surbs_last_received_at(pending_reply_target)
else {
error!("we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!", retransmission_buf.total_size());
error!(
"we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!",
retransmission_buf.total_size()
);
to_remove.push(*pending_reply_target);
continue;
};
@@ -702,7 +723,9 @@ where
// if client is offline)
if vals.current_clear_rerequest_counter > max_rerequests {
to_remove.push(*pending_reply_target);
debug!("we have reached the maximum threshold of attempting to request surbs from {pending_reply_target}. dropping the sender");
debug!(
"we have reached the maximum threshold of attempting to request surbs from {pending_reply_target}. dropping the sender"
);
continue;
}
@@ -710,7 +733,10 @@ where
if diff > max_drop_wait {
to_remove.push(*pending_reply_target)
} else {
debug!("We haven't received any surbs in {} from {pending_reply_target}. Going to explicitly ask for more", humantime::format_duration(diff.unsigned_abs()));
debug!(
"We haven't received any surbs in {} from {pending_reply_target}. Going to explicitly ask for more",
humantime::format_duration(diff.unsigned_abs())
);
vals.increment_current_clear_rerequest_counter();
to_request.push(*pending_reply_target);
}
@@ -4,8 +4,8 @@
use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement;
use futures::channel::{mpsc, oneshot};
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_task::connections::{ConnectionId, TransmissionLane};
use std::sync::Weak;
@@ -43,7 +43,9 @@ where
// 1. check whether we sent any surbs in the past to this recipient, otherwise
// they have no business in asking for more
if !self.tags_storage.exists(&recipient) {
warn!("{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!");
warn!(
"{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!"
);
return;
}
@@ -54,7 +56,12 @@ where
.reply_surbs
.maximum_allowed_reply_surb_request_size
{
warn!("The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...", self.config.reply_surbs.maximum_allowed_reply_surb_request_size);
warn!(
"The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...",
self.config
.reply_surbs
.maximum_allowed_reply_surb_request_size
);
amount = self
.config
.reply_surbs
@@ -23,7 +23,7 @@ use nym_sphinx::addressing::Recipient;
use nym_statistics_common::clients::{
ClientStatsController, ClientStatsReceiver, ClientStatsSender,
};
use nym_task::{connections::TransmissionLane, ShutdownToken, ShutdownTracker};
use nym_task::{ShutdownToken, ShutdownTracker, connections::TransmissionLane};
use std::time::Duration;
/// Time interval between reporting statistics locally (logging/shutdown_token)
@@ -5,8 +5,8 @@ use nym_sphinx::addressing::clients::Recipient;
use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError, NymTopologyMetadata};
use nym_validator_client::models::KeyRotationId;
use std::ops::Deref;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::{Notify, RwLock, RwLockReadGuard};
#[derive(Debug)]
@@ -63,7 +63,9 @@ impl TopologyRefresher {
trace!("Refreshing the topology");
if self.topology_accessor.controlled_manually() {
info!("topology is being controlled manually - we're going to wait until the control is released...");
info!(
"topology is being controlled manually - we're going to wait until the control is released..."
);
self.topology_accessor
.wait_for_released_manual_control()
.await;
@@ -138,6 +140,35 @@ impl TopologyRefresher {
}
}
pub async fn wait_for_initial_network(
&mut self,
timeout_duration: Duration,
) -> Result<(), NymTopologyError> {
info!(
"going to wait for at most {timeout_duration:?} for initial network to become online"
);
let deadline = sleep(timeout_duration);
tokio::pin!(deadline);
loop {
tokio::select! {
_ = &mut deadline => {
return Err(NymTopologyError::TimedOutWaitingForTopology)
}
_ = self.try_refresh() => {
if let Err(err) = self.ensure_topology_is_routable().await {
info!("network is still not routable...: {err}");
} else {
return Ok(())
}
sleep(self.refresh_rate).await
}
}
}
}
// it's perfectly fine if task is interrupted mid-refresh
// there's no data to persist or send over
pub async fn run(&mut self) {
@@ -3,8 +3,8 @@
use async_trait::async_trait;
use nym_mixnet_contract_common::EpochRewardedSet;
use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider};
use nym_topology::NymTopology;
use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider};
use nym_validator_client::nym_api::NymApiClientExt;
use rand::prelude::SliceRandom;
use rand::thread_rng;
@@ -82,7 +82,9 @@ impl NymApiTopologyProvider {
fn use_next_nym_api(&mut self) {
if self.nym_api_urls.len() == 1 {
warn!("There's only a single nym API available - it won't be possible to use a different one");
warn!(
"There's only a single nym API available - it won't be possible to use a different one"
);
return;
}
@@ -155,7 +157,10 @@ impl NymApiTopologyProvider {
let mixnodes = mixnodes_res.nodes;
if !gateways_res.metadata.consistency_check(&metadata) {
warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
warn!(
"inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}",
gateways_res.metadata
);
return None;
}
@@ -1,11 +1,11 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::helpers::{get_time_now, Instant};
use crate::client::helpers::{Instant, get_time_now};
use crate::client::real_messages_control::real_traffic_stream::RealMessage;
use nym_sphinx::chunking::fragment::Fragment;
use nym_task::connections::TransmissionLane;
use rand::{seq::SliceRandom, Rng};
use rand::{Rng, seq::SliceRandom};
use std::{
collections::{HashMap, HashSet, VecDeque},
time::Duration,
+13 -5
View File
@@ -7,9 +7,9 @@ use nym_gateway_client::error::GatewayClientError;
use nym_task::RegistryAccessError;
use nym_topology::node::RoutingNodeError;
use nym_topology::{NodeId, NymTopologyError};
use nym_validator_client::ValidatorClientError;
use nym_validator_client::nym_api::error::NymAPIError;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::ValidatorClientError;
use rand::distributions::WeightedError;
use std::error::Error;
use std::path::PathBuf;
@@ -56,7 +56,9 @@ pub enum ClientCoreError {
#[error("no gateways on network")]
NoGatewaysOnNetwork,
#[error("there are no more new gateways on the network - it seems this client has already registered with all nodes it could have")]
#[error(
"there are no more new gateways on the network - it seems this client has already registered with all nodes it could have"
)]
NoNewGatewaysAvailable,
#[error("list of nym apis is empty")]
@@ -127,7 +129,9 @@ pub enum ClientCoreError {
#[error("unexpected exit")]
UnexpectedExit,
#[error("this operation would have resulted in the gateway {gateway_id:?} key being overwritten without permission")]
#[error(
"this operation would have resulted in the gateway {gateway_id:?} key being overwritten without permission"
)]
ForbiddenGatewayKeyOverwrite { gateway_id: String },
#[error(
@@ -151,7 +155,9 @@ pub enum ClientCoreError {
#[error("attempted to obtain fresh gateway details whilst already knowing about one")]
UnexpectedGatewayDetails,
#[error("the provided gateway details (for gateway {gateway_id}) do not correspond to the shared keys")]
#[error(
"the provided gateway details (for gateway {gateway_id}) do not correspond to the shared keys"
)]
MismatchedGatewayDetails { gateway_id: String },
#[error("unable to upgrade config file from `{current_version}`")]
@@ -227,7 +233,9 @@ pub enum ClientCoreError {
source: url::ParseError,
},
#[error("this client (id: '{client_id}') has already been initialised before. If you want to add additional gateway, use `add-gateway` command")]
#[error(
"this client (id: '{client_id}') has already been initialised before. If you want to add additional gateway, use `add-gateway` command"
)]
AlreadyInitialised { client_id: String },
#[error("this client has already registered with gateway {gateway_id}")]
+5 -5
View File
@@ -5,13 +5,13 @@ use crate::error::ClientCoreError;
use crate::init::types::RegistrationResult;
use futures::{SinkExt, StreamExt};
use nym_crypto::asymmetric::ed25519;
use nym_gateway_client::client::GatewayListeners;
use nym_gateway_client::GatewayClient;
use nym_gateway_client::client::GatewayListeners;
use nym_topology::node::RoutingNode;
use nym_validator_client::UserAgent;
use nym_validator_client::client::{IdentityKeyRef, NymApiClientExt};
use nym_validator_client::nym_nodes::SkimmedNodesWithMetadata;
use nym_validator_client::UserAgent;
use rand::{seq::SliceRandom, Rng};
use rand::{Rng, seq::SliceRandom};
#[cfg(unix)]
use std::os::fd::RawFd;
use std::{sync::Arc, time::Duration};
@@ -28,10 +28,10 @@ use nym_wasm_utils::websocket::JSWebsocket;
#[cfg(not(target_arch = "wasm32"))]
use tokio::net::TcpStream;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::sleep;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::Instant;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::sleep;
#[cfg(not(target_arch = "wasm32"))]
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
#[cfg(target_arch = "wasm32")]
use wasmtimer::std::Instant;
+1 -1
View File
@@ -7,8 +7,8 @@ use crate::client::base_client::storage::helpers::{
has_gateway_details, load_active_gateway_details, load_client_keys, load_gateway_details,
store_gateway_details, update_stored_published_data_gateway,
};
use crate::client::key_manager::persistence::KeyStore;
use crate::client::key_manager::ClientKeys;
use crate::client::key_manager::persistence::KeyStore;
use crate::error::ClientCoreError;
use crate::init::helpers::{
choose_gateway_by_latency, get_specified_gateway, uniformly_random_gateway,
+2 -2
View File
@@ -1,8 +1,8 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::key_manager::persistence::KeyStore;
use crate::client::key_manager::ClientKeys;
use crate::client::key_manager::persistence::KeyStore;
use crate::config::Config;
use crate::error::ClientCoreError;
use crate::init::{setup_gateway, use_loaded_gateway_details};
@@ -10,8 +10,8 @@ use nym_client_core_gateways_storage::{
GatewayRegistration, GatewaysDetailsStore, RemoteGatewayDetails,
};
use nym_crypto::asymmetric::ed25519;
use nym_gateway_client::client::{GatewayListeners, InitGatewayClient};
use nym_gateway_client::SharedSymmetricKey;
use nym_gateway_client::client::{GatewayListeners, InitGatewayClient};
use nym_sphinx::addressing::clients::Recipient;
use nym_topology::node::RoutingNode;
use nym_validator_client::client::IdentityKey;
@@ -130,7 +130,7 @@ pub trait CosmWasmClient: TendermintRpcClient {
let req = QueryBalanceRequest {
address: address.to_string(),
denom: search_denom.to_string(),
denom: search_denom,
};
let res = self
@@ -199,6 +199,18 @@ impl NyxdClient<HttpClient, DirectSecp256k1HdWallet> {
let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic)?;
Ok(Self::connect_with_signer(config, client, wallet))
}
pub fn connect_with_mnemonic_and_network_details<U>(
endpoint: U,
network_details: NymNetworkDetails,
mnemonic: bip39::Mnemonic,
) -> Result<DirectSigningHttpRpcNyxdClient, NyxdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
let config = Config::try_from_nym_network_details(&network_details)?;
Self::connect_with_mnemonic(config, endpoint, mnemonic)
}
}
#[allow(deprecated)]
+11 -5
View File
@@ -22,6 +22,16 @@ pub struct ChainDetails {
pub stake_denom: DenomDetailsOwned,
}
impl ChainDetails {
pub fn mainnet() -> Self {
ChainDetails {
bech32_account_prefix: mainnet::BECH32_PREFIX.into(),
mix_denom: mainnet::MIX_DENOM.into(),
stake_denom: mainnet::STAKE_DENOM.into(),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct NymContracts {
@@ -181,11 +191,7 @@ impl NymNetworkDetails {
// Consider caching this process (lazy static)
NymNetworkDetails {
network_name: mainnet::NETWORK_NAME.into(),
chain_details: ChainDetails {
bech32_account_prefix: mainnet::BECH32_PREFIX.into(),
mix_denom: mainnet::MIX_DENOM.into(),
stake_denom: mainnet::STAKE_DENOM.into(),
},
chain_details: ChainDetails::mainnet(),
endpoints: mainnet::validators(),
contracts: NymContracts {
mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS),
+3
View File
@@ -38,6 +38,9 @@ pub enum NymTopologyError {
#[error("timed out while waiting for gateway '{identity_key}' to come online")]
TimedOutWaitingForGateway { identity_key: String },
#[error("timed out while waiting for minimum network topology to become online")]
TimedOutWaitingForTopology,
#[error(
"Wanted to create a mix route with {requested} hops, while only {available} layers are available"
)]
+10
View File
@@ -403,6 +403,10 @@ pub struct TopologyWasm {
/// before abandoning the procedure.
pub max_startup_gateway_waiting_period_ms: u32,
/// Defines how long the client is going to wait on startup for minimal topology to become online,
/// before abandoning the procedure.
pub max_startup_network_waiting_period_ms: u32,
/// Specifies whether the client should not refresh the network topology after obtaining
/// the first valid instance.
/// Supersedes `topology_refresh_rate_ms`.
@@ -446,6 +450,9 @@ impl From<TopologyWasm> for ConfigTopology {
max_startup_gateway_waiting_period: Duration::from_millis(
topology.max_startup_gateway_waiting_period_ms as u64,
),
max_startup_network_waiting_period: Duration::from_millis(
topology.max_startup_network_waiting_period_ms as u64,
),
minimum_mixnode_performance: topology.minimum_mixnode_performance,
minimum_gateway_performance: topology.minimum_gateway_performance,
use_extended_topology: topology.use_extended_topology,
@@ -463,6 +470,9 @@ impl From<ConfigTopology> for TopologyWasm {
max_startup_gateway_waiting_period_ms: topology
.max_startup_gateway_waiting_period
.as_millis() as u32,
max_startup_network_waiting_period_ms: topology
.max_startup_network_waiting_period
.as_millis() as u32,
disable_refreshing: topology.disable_refreshing,
minimum_mixnode_performance: topology.minimum_mixnode_performance,
minimum_gateway_performance: topology.minimum_gateway_performance,
@@ -276,6 +276,11 @@ pub struct TopologyWasmOverride {
#[tsify(optional)]
pub max_startup_gateway_waiting_period_ms: Option<u32>,
/// Defines how long the client is going to wait on startup for minimal topology to become online,
/// before abandoning the procedure.
#[tsify(optional)]
pub max_startup_network_waiting_period_ms: Option<u32>,
/// Specifies whether the client should not refresh the network topology after obtaining
/// the first valid instance.
/// Supersedes `topology_refresh_rate_ms`.
@@ -322,6 +327,9 @@ impl From<TopologyWasmOverride> for TopologyWasm {
max_startup_gateway_waiting_period_ms: value
.max_startup_gateway_waiting_period_ms
.unwrap_or(def.max_startup_gateway_waiting_period_ms),
max_startup_network_waiting_period_ms: value
.max_startup_network_waiting_period_ms
.unwrap_or(def.max_startup_network_waiting_period_ms),
disable_refreshing: value.disable_refreshing.unwrap_or(def.disable_refreshing),
minimum_mixnode_performance: value
.minimum_mixnode_performance
+5
View File
@@ -185,6 +185,7 @@ start_mixnode() {
container run \
--name "$container_name" \
--dns 1.1.1.1 \
-m 2G \
--network "$NETWORK_NAME" \
-p "${mixnet_port}:${mixnet_port}" \
@@ -226,6 +227,7 @@ start_gateway() {
container run \
--name "$GATEWAY_CONTAINER" \
--dns 1.1.1.1 \
-m 2G \
--network "$NETWORK_NAME" \
-p 9000:9000 \
@@ -365,6 +367,7 @@ start_network_requester() {
container run \
--name "$REQUESTER_CONTAINER" \
--dns 1.1.1.1 \
--network "$NETWORK_NAME" \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
@@ -400,6 +403,7 @@ start_socks5_client() {
container run \
--name "$SOCKS5_CONTAINER" \
--dns 1.1.1.1 \
--network "$NETWORK_NAME" \
-p 1080:1080 \
-v "$VOLUME_PATH:/localnet:ro" \
@@ -570,6 +574,7 @@ build_topology() {
# Run build_topology.py in a container with access to the volumes
container run \
--name "nym-localnet-topology-builder" \
--dns 1.1.1.1 \
--network "$NETWORK_NAME" \
-v "$VOLUME_PATH:/localnet" \
-v "$NYM_VOLUME_PATH:/root/.nym" \
@@ -0,0 +1,107 @@
# Single-stage Dockerfile for Nym localnet
# Builds: nym-node, nym-nym-api
# Target: Apple Container Runtime with host networking
# syntax=docker/dockerfile:1.4
FROM rust:1.91.1 AS builder
# Install runtime dependencies including Go for wireguard-go
#RUN apt update && apt install -y \
# python3 \
# python3-pip \
# netcat-openbsd \
# jq \
# iproute2 \
# net-tools \
# wireguard-tools \
# golang-go \
# git \
# && rm -rf /var/lib/apt/lists/*
RUN apt update && apt install -y \
iproute2 \
iptables \
netcat-openbsd \
net-tools \
wireguard-tools \
golang-go \
git \
jq \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*
# Install wireguard-go (userspace WireGuard implementation)
RUN git clone https://git.zx2c4.com/wireguard-go && \
cd wireguard-go && \
make && \
cp wireguard-go /usr/local/bin/ && \
cd .. && \
rm -rf wireguard-go
WORKDIR /usr/src/nym
###############################################################
# 1. Copy only top-level manifests (always stable)
###############################################################
COPY Cargo.toml Cargo.lock ./
###############################################################
# 2. Copy *full workspace* into a temp folder
###############################################################
COPY . /tmp/fullsrc
###############################################################
# 3. Recreate directory structure for all workspace crates
# by finding Cargo.toml files inside the container (for dependency caching)
###############################################################
RUN set -eux; \
cd /usr/src/nym; \
mkdir -p /usr/src/nym; \
# find every Cargo.toml except the top-level one \
find /tmp/fullsrc -name Cargo.toml ! -path "/tmp/fullsrc/Cargo.toml" | while read path; do \
rel="${path#/tmp/fullsrc/}"; \
dir="$(dirname "$rel")"; \
mkdir -p "$dir"; \
cp "$path" "$dir/"; \
done
###############################################################
# 4. Dummy build (dependencies only)
###############################################################
RUN echo "fn main() {}" > dummy.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/usr/src/nym/target \
cargo build --release --locked || true
###############################################################
# 5. Copy REAL workspace sources into place
###############################################################
RUN cp -a /tmp/fullsrc/. /usr/src/nym/
###############################################################
# 6. Final build
###############################################################
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/usr/src/nym/target \
cargo build --release --locked \
-p nym-node \
-p nym-api \
-p nym-gateway-probe
# Move binaries to /usr/local/bin for easy access
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/usr/src/nym/target \
mv /usr/src/nym/target/release/nym-node /usr/local/bin/ && \
mv /usr/src/nym/target/release/nym-api /usr/local/bin/ && \
mv /usr/src/nym/target/release/nym-gateway-probe /usr/local/bin/
WORKDIR /nym
# Default command
CMD ["nym-node", "--help"]
@@ -19,6 +19,7 @@ pub async fn create_mixnet_client(
custom_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
wait_for_gateway: bool,
wait_for_topology: bool,
paths: &CommonClientPaths,
) -> Result<nym_sdk::mixnet::MixnetClient, AuthenticatorError> {
let debug_config = config.debug;
@@ -34,7 +35,8 @@ pub async fn create_mixnet_client(
.network_details(NymNetworkDetails::new_from_env())
.debug_config(debug_config)
.custom_shutdown(shutdown)
.with_wait_for_gateway(wait_for_gateway);
.with_wait_for_gateway(wait_for_gateway)
.with_wait_for_initial_topology(wait_for_topology);
if !config.get_disabled_credentials_mode() {
client_builder = client_builder.enable_credentials_mode();
}
@@ -36,6 +36,7 @@ pub struct Authenticator {
peer_registrator: PeerRegistrator,
upgrade_mode_state: UpgradeModeDetails,
wait_for_gateway: bool,
wait_for_topology: bool,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
wireguard_gateway_data: WireguardGatewayData,
@@ -56,6 +57,7 @@ impl Authenticator {
peer_registrator,
upgrade_mode_state,
wait_for_gateway: false,
wait_for_topology: false,
custom_topology_provider: None,
custom_gateway_transceiver: None,
wireguard_gateway_data,
@@ -71,6 +73,13 @@ impl Authenticator {
self
}
#[must_use]
#[allow(unused)]
pub fn with_wait_for_initial_topology(mut self, wait_for_initial_topology: bool) -> Self {
self.wait_for_topology = wait_for_initial_topology;
self
}
#[must_use]
pub fn with_minimum_gateway_performance(mut self, minimum_gateway_performance: u8) -> Self {
self.config.base.debug.topology.minimum_gateway_performance = minimum_gateway_performance;
@@ -123,6 +132,7 @@ impl Authenticator {
self.custom_gateway_transceiver,
self.custom_topology_provider,
self.wait_for_gateway,
self.wait_for_topology,
&self.config.storage_paths.common_paths,
)
.await?;
+3
View File
@@ -354,6 +354,7 @@ impl GatewayTasksBuilder {
NRServiceProviderBuilder::new(nr_opts.config.clone(), self.shutdown_tracker.clone())
.with_custom_gateway_transceiver(transceiver)
.with_wait_for_gateway(true)
.with_wait_for_initial_topology(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(topology_provider)
.with_on_start(on_start_tx);
@@ -389,6 +390,7 @@ impl GatewayTasksBuilder {
IpPacketRouter::new(ip_opts.config.clone(), self.shutdown_tracker.clone())
.with_custom_gateway_transceiver(Box::new(transceiver))
.with_wait_for_gateway(true)
.with_wait_for_initial_topology(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(topology_provider)
.with_on_start(on_start_tx);
@@ -488,6 +490,7 @@ impl GatewayTasksBuilder {
)
.with_custom_gateway_transceiver(transceiver)
.with_wait_for_gateway(true)
.with_wait_for_initial_topology(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(topology_provider)
.with_on_start(on_start_tx);
@@ -17,6 +17,7 @@ pub mod network;
pub mod network_monitor;
pub mod node_status;
pub mod schema_helpers;
pub mod utility;
// don't break existing imports
pub use api_status::*;
@@ -0,0 +1,40 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RefreshMixnetContractCacheRequestBody {
// for now no additional data is needed, but keep the struct for easier changes down the line
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RefreshMixnetContractCacheResponse {
pub success: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct MixnetContractCacheTimestampResponse {
#[serde(with = "time::serde::rfc3339")]
#[schema(value_type = String)]
pub timestamp: OffsetDateTime,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RefreshNodeStatusCacheRequestBody {
// for now no additional data is needed, but keep the struct for easier changes down the line
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct RefreshNodeStatusCacheResponse {
pub success: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct NodeStatusCacheTimestampResponse {
#[serde(with = "time::serde::rfc3339")]
#[schema(value_type = String)]
pub timestamp: OffsetDateTime,
}
+27 -3
View File
@@ -5,15 +5,20 @@ use crate::ecash::api_routes::handlers::ecash_routes;
use crate::ecash::error::{EcashError, Result};
use crate::ecash::keys::KeyPairWithEpoch;
use crate::ecash::state::EcashState;
use crate::mixnet_contract_cache::cache::MixnetContractCache;
use crate::network::models::NetworkDetails;
use crate::node_describe_cache::cache::DescribedNodes;
use crate::node_status_api::handlers::unstable;
use crate::node_status_api::NodeStatusCache;
use crate::status::ApiStatusState;
use crate::support::caching::cache::SharedCache;
use crate::support::caching::refresher::RefreshRequester;
use crate::support::config;
use crate::support::http::state::chain_status::ChainStatusCache;
use crate::support::http::state::contract_details::ContractDetailsCache;
use crate::support::http::state::force_refresh::ForcedRefresh;
use crate::support::http::state::mixnet_contract_cache::MixnetContractCacheState;
use crate::support::http::state::node_annotations_cache::NodeAnnotationsCache;
use crate::support::http::state::AppState;
use crate::support::nyxd::Client;
use crate::support::storage::NymApiStorage;
@@ -1275,15 +1280,29 @@ impl TestFixture {
storage: NymApiStorage,
ecash_state: EcashState,
nyxd_client: Client,
tmp_dir: &TempDir,
) -> AppState {
let mixnet_contract_paths = tmp_dir.path().join("mixnet_contract");
let node_annotations_paths = tmp_dir.path().join("node_annotations");
let mixnet_contract_cache_state =
MixnetContractCache::new(&mixnet_contract_paths, Duration::from_secs(42));
let mixnet_contract_cache =
MixnetContractCacheState::new(mixnet_contract_cache_state, RefreshRequester::default());
let node_status_cache_state =
NodeStatusCache::new(&node_annotations_paths, Duration::from_secs(42));
let node_annotations_cache =
NodeAnnotationsCache::new(node_status_cache_state, RefreshRequester::default());
AppState {
nyxd_client,
chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)),
ecash_signers_cache: Default::default(),
address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000),
forced_refresh: ForcedRefresh::new(true),
mixnet_contract_cache: SharedCache::new().into(),
node_status_cache: SharedCache::new().into(),
mixnet_contract_cache,
node_annotations_cache,
storage,
described_nodes_cache: SharedCache::<DescribedNodes>::new(),
network_details: NetworkDetails::new(
@@ -1366,7 +1385,12 @@ impl TestFixture {
TestFixture {
axum: TestServer::new(Router::new().nest("/v1/ecash", ecash_routes()).with_state(
Self::build_app_state(storage.clone(), ecash_state, another_fake_nyxd_client),
Self::build_app_state(
storage.clone(),
ecash_state,
another_fake_nyxd_client,
&tmp_dir,
),
))
.unwrap(),
storage,
+1
View File
@@ -24,6 +24,7 @@ mod signers_cache;
mod status;
pub(crate) mod support;
mod unstable_routes;
pub(crate) mod utility_routes;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
@@ -174,6 +174,33 @@ impl PacketPreparer {
layer_choices.choose(rng).copied().unwrap()
}
fn naive_rearrange(
&self,
nodes: HashMap<LegacyMixLayer, Vec<(RoutingNode, f64)>>,
) -> HashMap<LegacyMixLayer, Vec<(RoutingNode, f64)>> {
let all_nodes = nodes
.into_values()
.flat_map(|nodes| nodes.into_iter())
.collect::<Vec<_>>();
let mut layered = HashMap::new();
for (i, node) in all_nodes.into_iter().enumerate() {
let layer = match i % 3 {
0 => LegacyMixLayer::One,
1 => LegacyMixLayer::Two,
2 => LegacyMixLayer::Three,
// this is literally impossible to reach
#[allow(clippy::unreachable)]
_ => unreachable!(),
};
let layer_mixes = layered.entry(layer).or_insert_with(Vec::new);
layer_mixes.push(node)
}
layered
}
fn to_legacy_layered_mixes<'a, R: Rng>(
&self,
rng: &mut R,
@@ -199,7 +226,20 @@ impl PacketPreparer {
layer_mixes.push((parsed_node, weight))
}
layered_mixes
// if some layers are empty, fallback to naive assignment
// (for small localnets/testnets)
let layers = [
LegacyMixLayer::One,
LegacyMixLayer::Two,
LegacyMixLayer::Three,
];
if layers.into_iter().any(|l| !layered_mixes.contains_key(&l)) {
info!("insufficient number of nodes on layers - attempting to fallback to naive assignment");
self.naive_rearrange(layered_mixes)
} else {
layered_mixes
}
}
fn to_legacy_gateway_nodes<'a>(
+4
View File
@@ -78,6 +78,10 @@ impl NodeStatusCacheRefresher {
}
}
pub(crate) fn refresh_requester(&self) -> RefreshRequester {
self.refresh_requester.clone()
}
/// Runs the node status cache refresher task.
pub async fn run(&mut self, shutdown_token: ShutdownToken) {
let mut last_update = OffsetDateTime::now_utc();
+4 -1
View File
@@ -5,6 +5,7 @@ use self::cache::refresher::NodeStatusCacheRefresher;
use crate::node_describe_cache::cache::DescribedNodes;
use crate::node_performance::provider::NodePerformanceProvider;
use crate::support::caching::cache::SharedCache;
use crate::support::caching::refresher::RefreshRequester;
use crate::support::config;
use crate::{
mixnet_contract_cache::cache::MixnetContractCache,
@@ -42,7 +43,7 @@ pub(crate) fn start_cache_refresh(
described_cache_cache_listener: watch::Receiver<support::caching::CacheNotification>,
on_disk_file: PathBuf,
shutdown_manager: &ShutdownManager,
) {
) -> RefreshRequester {
let mut nym_api_cache_refresher = NodeStatusCacheRefresher::new(
node_status_cache_state.to_owned(),
config.debug.caching_interval,
@@ -53,6 +54,8 @@ pub(crate) fn start_cache_refresh(
performance_provider,
on_disk_file,
);
let refresh_requester = nym_api_cache_refresher.refresh_requester();
let shutdown_listener = shutdown_manager.clone_shutdown_token();
tokio::spawn(async move { nym_api_cache_refresher.run(shutdown_listener).await });
refresh_requester
}
+1 -1
View File
@@ -283,7 +283,7 @@ where
_ = refresh_interval.tick() => self.refresh(&shutdown_token).await,
// note: `Notify` is not cancellation safe, HOWEVER, there's only one listener,
// so it doesn't matter if we lose our queue position
_ = self.refresh_requester.0.notified() => {
_ = self.refresh_requester.notified() => {
self.refresh(&shutdown_token).await;
// since we just performed the full request, we can reset our existing interval
refresh_interval.reset();
+12 -6
View File
@@ -1,6 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::support::cli::run::initialise_storage;
use crate::support::config::default_config_filepath;
use crate::support::config::helpers::initialise_new;
use anyhow::bail;
@@ -71,8 +72,10 @@ pub(crate) struct Args {
#[clap(hide = true, long, default_value_t = false)]
pub(crate) allow_illegal_ips: bool,
// #[clap(short, long, default_value_t = OutputFormat::default())]
// output: OutputFormat,
/// Bearer token for exposing and accessing additional utility routes
#[clap(long, env = "NYMAPI_UTILITY_ROUTES_BEARER_ARG")]
pub(crate) utility_routes_bearer: Option<String>,
}
pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
@@ -88,12 +91,15 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
bail!("there already exists a configuration file at '{}'. If you intend to replace it, you need to manually remove it first. Make sure to make backup of any keys and datastores first.", config_path.display())
}
let config = initialise_new(&args.id)?;
// args take precedence over env
config
let config = initialise_new(&args.id)?
.override_with_env()
.override_with_args(args)
.try_save()?;
.override_with_args(args);
config.try_save()?;
// create the initial database file
initialise_storage(&config).await?;
Ok(())
}
+57 -35
View File
@@ -26,6 +26,8 @@ use crate::support::config::{Config, DEFAULT_CHAIN_STATUS_CACHE_TTL};
use crate::support::http::state::chain_status::ChainStatusCache;
use crate::support::http::state::contract_details::ContractDetailsCache;
use crate::support::http::state::force_refresh::ForcedRefresh;
use crate::support::http::state::mixnet_contract_cache::MixnetContractCacheState;
use crate::support::http::state::node_annotations_cache::NodeAnnotationsCache;
use crate::support::http::state::AppState;
use crate::support::http::{RouterBuilder, TASK_MANAGER_TIMEOUT_S};
use crate::support::nyxd;
@@ -117,20 +119,33 @@ pub(crate) struct Args {
#[clap(hide = true, long, default_value_t = false)]
pub(crate) allow_illegal_ips: bool,
/// Bearer token for exposing and accessing additional utility routes
#[clap(long, env = "NYMAPI_UTILITY_ROUTES_BEARER_ARG")]
pub(crate) utility_routes_bearer: Option<String>,
}
async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownManager> {
pub(crate) async fn initialise_storage(config: &Config) -> anyhow::Result<NymApiStorage> {
let nyxd_client = nyxd::Client::new(config)?;
let storage = NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?;
// try to perform any needed migrations of the storage
migrate_to_directory_services_v2_1(&storage, &nyxd_client).await?;
Ok(storage)
}
async fn start_nym_api_tasks(mut config: Config) -> anyhow::Result<ShutdownManager> {
let shutdown_manager = ShutdownManager::build_new_default()?
.with_shutdown_duration(Duration::from_secs(TASK_MANAGER_TIMEOUT_S));
let nyxd_client = nyxd::Client::new(config)?;
let nyxd_client = nyxd::Client::new(&config)?;
let connected_nyxd = config.get_nyxd_url();
let nym_network_details = NymNetworkDetails::new_from_env();
let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details);
let ecash_keypair_wrapper = ecash::keys::KeyPair::new();
// if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen
// if the keypair doesn't exist (because say this API is running in the caching mode), nothing will happen
if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.ecash_signer)? {
let issued_for = loaded_keys.issued_for_epoch;
ecash_keypair_wrapper.set(loaded_keys).await;
@@ -140,16 +155,11 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownManager>
}
}
let storage = NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?;
// try to perform any needed migrations of the storage
migrate_to_directory_services_v2_1(&storage, &nyxd_client).await?;
let storage = initialise_storage(&config).await?;
let identity_keypair = config.base.storage_paths.load_identity()?;
let identity_public_key = *identity_keypair.public_key();
let router = RouterBuilder::with_default_routes(config.network_monitor.enabled);
let mix_denom = network_details.network.chain_details.mix_denom.base.clone();
let storage_cfg = &config.base.storage_paths;
@@ -216,7 +226,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownManager>
let encoded_identity = identity_keypair.public_key().to_base58_string();
let mut ecash_state = EcashState::new(
config,
&config,
ecash_contract,
nyxd_client.clone(),
identity_keypair,
@@ -255,25 +265,6 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownManager>
};
ecash_state.spawn_background_cleaner();
let router = router.with_state(AppState {
nyxd_client: nyxd_client.clone(),
chain_status_cache: ChainStatusCache::new(DEFAULT_CHAIN_STATUS_CACHE_TTL),
ecash_signers_cache,
address_info_cache: AddressInfoCache::new(
config.address_cache.time_to_live,
config.address_cache.capacity,
),
forced_refresh: ForcedRefresh::new(config.describe_cache.debug.allow_illegal_ips),
mixnet_contract_cache: mixnet_contract_cache_state.clone(),
node_status_cache: node_status_cache_state.clone(),
storage: storage.clone(),
described_nodes_cache: described_nodes_cache.clone(),
network_details: network_details.clone(),
node_info_cache,
contract_info_cache: ContractDetailsCache::new(config.contracts_info_cache.time_to_live),
api_status: ApiStatusState::new(signer_information),
ecash_state: Arc::new(ecash_state),
});
let describe_cache_refresh_requester = describe_cache_refresher.refresh_requester();
@@ -313,7 +304,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownManager>
let contract_cache_watcher =
mixnet_contract_cache_refresher.start_with_watcher(shutdown_manager.clone_shutdown_token());
node_status_api::start_cache_refresh(
let node_status_cache_refresh_requester = node_status_api::start_cache_refresh(
&config.node_status_api,
&mixnet_contract_cache_state,
&described_nodes_cache,
@@ -347,7 +338,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownManager>
// if the monitoring is enabled
if config.network_monitor.enabled {
network_monitor::start::<SphinxMessageReceiver>(
config,
&config,
&mixnet_contract_cache_state,
described_nodes_cache.clone(),
node_status_cache_state.clone(),
@@ -364,9 +355,9 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownManager>
if config.rewarding.enabled && has_performance_data {
epoch_operations::ensure_rewarding_permission(&nyxd_client).await?;
EpochAdvancer::start(
nyxd_client,
nyxd_client.clone(),
&mixnet_contract_cache_state,
mixnet_contract_cache_refresh_requester,
mixnet_contract_cache_refresh_requester.clone(),
&node_status_cache_state,
described_nodes_cache.clone(),
&storage,
@@ -379,10 +370,41 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownManager>
KeyRotationController::new(
describe_cache_refresh_requester,
contract_cache_watcher,
mixnet_contract_cache_state,
mixnet_contract_cache_state.clone(),
)
.start(shutdown_manager.clone_shutdown_token());
let mixnet_contract_cache = MixnetContractCacheState::new(
mixnet_contract_cache_state,
mixnet_contract_cache_refresh_requester,
);
let node_annotations_cache =
NodeAnnotationsCache::new(node_status_cache_state, node_status_cache_refresh_requester);
let router = RouterBuilder::with_default_routes(
config.network_monitor.enabled,
config.base.utility_routes_bearer.take(),
)
.with_state(AppState {
nyxd_client,
chain_status_cache: ChainStatusCache::new(DEFAULT_CHAIN_STATUS_CACHE_TTL),
ecash_signers_cache,
address_info_cache: AddressInfoCache::new(
config.address_cache.time_to_live,
config.address_cache.capacity,
),
forced_refresh: ForcedRefresh::new(config.describe_cache.debug.allow_illegal_ips),
mixnet_contract_cache,
node_annotations_cache,
storage,
described_nodes_cache,
network_details,
node_info_cache,
contract_info_cache: ContractDetailsCache::new(config.contracts_info_cache.time_to_live),
api_status: ApiStatusState::new(signer_information),
ecash_state: Arc::new(ecash_state),
});
let bind_address = config.base.bind_address.to_owned();
let server = router.build_server(&bind_address).await?;
@@ -407,7 +429,7 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
config.validate_and_fixup()?;
let mut shutdown_manager = start_nym_api_tasks(&config).await?;
let mut shutdown_manager = start_nym_api_tasks(config).await?;
shutdown_manager.run_until_shutdown().await;
Ok(())
+9
View File
@@ -200,6 +200,9 @@ impl Config {
if let Some(nyxd_upstream) = args.nyxd_validator {
self.base.local_validator = nyxd_upstream;
}
if let Some(bearer) = args.utility_routes_bearer {
self.base.utility_routes_bearer = Some(bearer)
}
if let Some(mnemonic) = args.mnemonic {
self.base.mnemonic = Some(mnemonic)
}
@@ -307,6 +310,11 @@ pub struct Base {
#[serde(default = "default_http_socket_addr")]
pub bind_address: SocketAddr,
/// Bearer token for exposing and accessing additional utility routes
#[serde(default)]
#[serde(deserialize_with = "de_maybe_stringified")]
pub utility_routes_bearer: Option<String>,
/// Mnemonic used for rewarding and/or multisig operations
// TODO: similarly to the note in gateway, this should get moved to a separate file
#[serde(deserialize_with = "de_maybe_stringified")]
@@ -332,6 +340,7 @@ impl Base {
id,
local_validator: default_validator,
bind_address: default_http_socket_addr(),
utility_routes_bearer: None,
mnemonic: None,
}
}
+6
View File
@@ -15,6 +15,9 @@ pub(crate) struct OverrideConfig {
/// Endpoint to nyxd instance used for contract information.
pub(crate) nyxd_validator: Option<url::Url>,
/// Bearer token for exposing and accessing additional utility routes
pub(crate) utility_routes_bearer: Option<String>,
/// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions
pub(crate) mnemonic: Option<bip39::Mnemonic>,
@@ -33,6 +36,7 @@ pub(crate) struct OverrideConfig {
pub(crate) bind_address: Option<SocketAddr>,
pub(crate) address_cache_ttl: Option<Duration>,
pub(crate) address_cache_capacity: Option<u64>,
pub(crate) allow_illegal_ips: bool,
@@ -44,6 +48,7 @@ impl From<init::Args> for OverrideConfig {
enable_monitor: Some(args.enable_monitor),
enable_rewarding: Some(args.enable_rewarding),
nyxd_validator: args.nyxd_validator,
utility_routes_bearer: args.utility_routes_bearer,
mnemonic: args.mnemonic,
enable_zk_nym: Some(args.enable_zk_nym),
announce_address: args.announce_address,
@@ -63,6 +68,7 @@ impl From<run::Args> for OverrideConfig {
enable_monitor: args.enable_monitor,
enable_rewarding: args.enable_rewarding,
nyxd_validator: args.nyxd_validator,
utility_routes_bearer: args.utility_routes_bearer,
mnemonic: args.mnemonic,
enable_zk_nym: args.enable_zk_nym,
announce_address: args.announce_address,
+3
View File
@@ -18,6 +18,9 @@ local_validator = '{{ base.local_validator }}'
# Socket address this api will use for binding its http API.
bind_address = '{{ base.bind_address }}'
# Bearer token for exposing and accessing additional utility routes
utility_routes_bearer = '{{ base.utility_routes_bearer }}'
# Mnemonic used for rewarding and validator interaction
mnemonic = '{{ base.mnemonic }}'
+39 -22
View File
@@ -11,18 +11,22 @@ use crate::support::http::state::AppState;
use crate::unstable_routes::v1::unstable_routes_v1;
use crate::unstable_routes::v2::unstable_routes_v2;
use crate::unstable_routes::v3::unstable_routes_v3;
use crate::utility_routes::utility_routes;
use crate::{nym_nodes, status};
use anyhow::anyhow;
use axum::response::Redirect;
use axum::routing::get;
use axum::Router;
use core::net::SocketAddr;
use nym_http_api_common::middleware::bearer_auth::AuthLayer;
use nym_http_api_common::middleware::logging::log_request_info;
use nym_task::ShutdownToken;
use std::sync::Arc;
use tokio::net::TcpListener;
use tower_http::cors::CorsLayer;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use zeroize::Zeroizing;
/// Wrapper around `axum::Router` which ensures correct [order of layers][order].
/// Add new routes as if you were working directly with `axum`.
@@ -36,9 +40,40 @@ pub(crate) struct RouterBuilder {
}
impl RouterBuilder {
fn v1_routes(network_monitor: bool, bearer_token: Option<String>) -> Router<AppState> {
let base = Router::new()
// unfortunately some routes didn't use correct prefix and were attached to the root
.nest("/epoch", epoch_routes())
.nest("/circulating-supply", circulating_supply_routes())
.nest("/status", status_routes(network_monitor))
.nest("/network", nym_network_routes())
.nest("/api-status", status::handlers::api_status_routes())
.nest("/nym-nodes", nym_nodes::handlers::v1::routes())
.nest("/ecash", ecash_routes())
.nest("/unstable", unstable_routes_v1())
.nest("/legacy", legacy_nodes_routes()); // CORS layer needs to be "outside" of routes
if let Some(bearer_token) = bearer_token {
let auth_middleware = AuthLayer::new(Arc::new(Zeroizing::new(bearer_token)));
base.nest("/utility", utility_routes().route_layer(auth_middleware))
} else {
base
}
}
fn v2_routes() -> Router<AppState> {
Router::new()
.nest("/unstable", unstable_routes_v2())
.nest("/nym-nodes", nym_nodes::handlers::v2::routes())
}
fn v3_routes() -> Router<AppState> {
Router::new().nest("/unstable", unstable_routes_v3())
}
/// All routes should be, if possible, added here. Exceptions are e.g.
/// routes which are added conditionally in other places based on some `if`.
pub(crate) fn with_default_routes(network_monitor: bool) -> Self {
pub(crate) fn with_default_routes(network_monitor: bool, bearer_token: Option<String>) -> Self {
// https://docs.rs/tower-http/0.1.1/tower_http/trace/index.html
// TODO rocket use tracing instead of env_logger
// https://github.com/tokio-rs/axum/blob/main/examples/tracing-aka-logging/src/main.rs
@@ -52,27 +87,9 @@ impl RouterBuilder {
let default_routes = Router::new()
.merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi()))
.route("/", get(|| async { Redirect::to("/swagger") }))
.nest(
"/v1",
Router::new()
// unfortunately some routes didn't use correct prefix and were attached to the root
.nest("/epoch", epoch_routes())
.nest("/circulating-supply", circulating_supply_routes())
.nest("/status", status_routes(network_monitor))
.nest("/network", nym_network_routes())
.nest("/api-status", status::handlers::api_status_routes())
.nest("/nym-nodes", nym_nodes::handlers::v1::routes())
.nest("/ecash", ecash_routes())
.nest("/unstable", unstable_routes_v1())
.nest("/legacy", legacy_nodes_routes()), // CORS layer needs to be "outside" of routes
)
.nest(
"/v2",
Router::new()
.nest("/unstable", unstable_routes_v2())
.nest("/nym-nodes", nym_nodes::handlers::v2::routes()),
)
.nest("/v3", Router::new().nest("/unstable", unstable_routes_v3()));
.nest("/v1", Self::v1_routes(network_monitor, bearer_token))
.nest("/v2", Self::v2_routes())
.nest("/v3", Self::v3_routes());
Self {
unfinished_router: default_routes,
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::support::caching::refresher::RefreshRequester;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::Arc;
use time::OffsetDateTime;
#[derive(Clone)]
pub(crate) struct Refreshing {
handle: RefreshRequester,
last_requested: Arc<AtomicI64>, // unix timestamp
}
impl Refreshing {
pub(crate) fn new(handle: RefreshRequester) -> Self {
Refreshing {
handle,
last_requested: Arc::new(Default::default()),
}
}
pub(crate) fn last_requested(&self) -> OffsetDateTime {
// SAFETY: this value is always populated with valid timestamps
#[allow(clippy::unwrap_used)]
OffsetDateTime::from_unix_timestamp(self.last_requested.load(Ordering::SeqCst)).unwrap()
}
fn update_last_requested(&self, now: OffsetDateTime) {
self.last_requested
.store(now.unix_timestamp(), Ordering::SeqCst);
}
pub(crate) fn request_refresh(&self, now: OffsetDateTime) {
self.update_last_requested(now);
self.handle.request_cache_refresh();
}
}
@@ -0,0 +1,21 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::mixnet_contract_cache::cache::MixnetContractCache;
use crate::support::caching::refresher::RefreshRequester;
use crate::support::http::state::helpers::Refreshing;
#[derive(Clone)]
pub(crate) struct MixnetContractCacheState {
pub(crate) inner_cache: MixnetContractCache,
pub(crate) refresh_handle: Refreshing,
}
impl MixnetContractCacheState {
pub(crate) fn new(inner_cache: MixnetContractCache, refresh_handle: RefreshRequester) -> Self {
MixnetContractCacheState {
inner_cache,
refresh_handle: Refreshing::new(refresh_handle),
}
}
}
+28 -5
View File
@@ -15,6 +15,8 @@ use crate::support::caching::Cache;
use crate::support::http::state::chain_status::ChainStatusCache;
use crate::support::http::state::contract_details::ContractDetailsCache;
use crate::support::http::state::force_refresh::ForcedRefresh;
use crate::support::http::state::mixnet_contract_cache::MixnetContractCacheState;
use crate::support::http::state::node_annotations_cache::NodeAnnotationsCache;
use crate::support::nyxd::Client;
use crate::support::storage;
use crate::unstable_routes::v1::account::cache::AddressInfoCache;
@@ -28,6 +30,9 @@ use tokio::sync::RwLockReadGuard;
pub(crate) mod chain_status;
pub(crate) mod contract_details;
pub(crate) mod force_refresh;
pub(crate) mod helpers;
pub(crate) mod mixnet_contract_cache;
pub(crate) mod node_annotations_cache;
#[derive(Clone)]
pub(crate) struct AppState {
@@ -52,11 +57,11 @@ pub(crate) struct AppState {
pub(crate) forced_refresh: ForcedRefresh,
/// Holds cached state of the Nym Mixnet contract, e.g. bonded nym-nodes, rewarded set, current interval.
pub(crate) mixnet_contract_cache: MixnetContractCache,
pub(crate) mixnet_contract_cache: MixnetContractCacheState,
/// Holds processed information on network nodes, i.e. performance, config scores, etc.
// TODO: also perhaps redundant?
pub(crate) node_status_cache: NodeStatusCache,
pub(crate) node_annotations_cache: NodeAnnotationsCache,
/// Holds reference to the persistent storage of this nym-api.
pub(crate) storage: storage::NymApiStorage,
@@ -98,11 +103,29 @@ impl FromRef<AppState> for Arc<EcashState> {
}
impl FromRef<AppState> for MixnetContractCache {
fn from_ref(app_state: &AppState) -> Self {
app_state.mixnet_contract_cache.inner_cache.clone()
}
}
impl FromRef<AppState> for MixnetContractCacheState {
fn from_ref(app_state: &AppState) -> Self {
app_state.mixnet_contract_cache.clone()
}
}
impl FromRef<AppState> for NodeAnnotationsCache {
fn from_ref(app_state: &AppState) -> Self {
app_state.node_annotations_cache.clone()
}
}
impl FromRef<AppState> for NodeStatusCache {
fn from_ref(app_state: &AppState) -> Self {
app_state.node_annotations_cache.inner_cache.clone()
}
}
impl FromRef<AppState> for SharedCache<SignersCacheData> {
fn from_ref(app_state: &AppState) -> Self {
app_state.ecash_signers_cache.clone()
@@ -117,11 +140,11 @@ impl AppState {
}
pub(crate) fn nym_contract_cache(&self) -> &MixnetContractCache {
&self.mixnet_contract_cache
&self.mixnet_contract_cache.inner_cache
}
pub(crate) fn node_status_cache(&self) -> &NodeStatusCache {
&self.node_status_cache
&self.node_annotations_cache.inner_cache
}
pub(crate) fn network_details(&self) -> &NetworkDetails {
@@ -173,7 +196,7 @@ impl AppState {
.address_info_cache
.collect_balances(
self.nyxd_client.clone(),
self.mixnet_contract_cache.clone(),
self.mixnet_contract_cache.inner_cache.clone(),
self.network_details()
.network
.chain_details
@@ -0,0 +1,21 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::NodeStatusCache;
use crate::support::caching::refresher::RefreshRequester;
use crate::support::http::state::helpers::Refreshing;
#[derive(Clone)]
pub(crate) struct NodeAnnotationsCache {
pub(crate) inner_cache: NodeStatusCache,
pub(crate) refresh_handle: Refreshing,
}
impl NodeAnnotationsCache {
pub(crate) fn new(inner_cache: NodeStatusCache, refresh_handle: RefreshRequester) -> Self {
NodeAnnotationsCache {
inner_cache,
refresh_handle: Refreshing::new(refresh_handle),
}
}
}
+170
View File
@@ -0,0 +1,170 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::mixnet_contract_cache::cache::MixnetContractCache;
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::node_status_api::NodeStatusCache;
use crate::support::http::state::mixnet_contract_cache::MixnetContractCacheState;
use crate::support::http::state::node_annotations_cache::NodeAnnotationsCache;
use crate::support::http::state::AppState;
use axum::extract::{Query, State};
use axum::routing::{get, post};
use axum::Router;
use nym_api_requests::models::utility::{
MixnetContractCacheTimestampResponse, NodeStatusCacheTimestampResponse,
RefreshMixnetContractCacheRequestBody, RefreshMixnetContractCacheResponse,
RefreshNodeStatusCacheRequestBody, RefreshNodeStatusCacheResponse,
};
use nym_http_api_common::{FormattedResponse, OutputParams};
use std::time::Duration;
use time::OffsetDateTime;
pub(crate) fn utility_routes() -> Router<AppState> {
Router::new()
.route("/refresh-mixnet-cache", post(refresh_mixnet_cache))
.route("/mixnet-cache-timestamp", get(mixnet_cache_timestamp))
.route(
"/refresh-node-annotations-cache",
post(refresh_node_annotations_cache),
)
.route(
"/node-annotations-cache-timestamp",
get(node_annotations_cache_timestamp),
)
}
/// Allow to request to refresh the cache of all mixnet nodes on the network.
/// Note that this endpoint enforces high global rate limiting and realistically
/// should not be used outside very special scenarios.
#[utoipa::path(
tag = "Utility",
post,
request_body = RefreshMixnetContractCacheRequestBody,
path = "/refresh-mixnet-cache",
context_path = "/v1/utility",
responses(
(status = 200, content(
(RefreshMixnetContractCacheResponse = "application/json"),
(RefreshMixnetContractCacheResponse = "application/yaml"),
(RefreshMixnetContractCacheResponse = "application/bincode")
))
),
params(OutputParams),
security(
("auth_token" = [])
)
)]
async fn refresh_mixnet_cache(
Query(output): Query<OutputParams>,
State(cache): State<MixnetContractCacheState>,
) -> AxumResult<FormattedResponse<RefreshMixnetContractCacheResponse>> {
let output = output.get_output();
let now = OffsetDateTime::now_utc();
// max 1 refresh every 5min (TODO: make it configurable)
let cutoff = now - Duration::from_secs(5 * 60);
let last = cache.refresh_handle.last_requested();
if last > cutoff {
return Err(AxumErrorResponse::too_many(
"already refreshed contract cache in the last 5 minutes",
));
}
cache.refresh_handle.request_refresh(now);
Ok(output.to_response(RefreshMixnetContractCacheResponse { success: true }))
}
/// Return information on when the mixnet cache has last been refreshed.
#[utoipa::path(
tag = "Utility",
get,
path = "/mixnet-cache-timestamp",
context_path = "/v1/utility",
responses(
(status = 200, content(
(MixnetContractCacheTimestampResponse = "application/json"),
(MixnetContractCacheTimestampResponse = "application/yaml"),
(MixnetContractCacheTimestampResponse = "application/bincode")
))
),
params(OutputParams),
security(
("auth_token" = [])
)
)]
async fn mixnet_cache_timestamp(
Query(output): Query<OutputParams>,
State(cache): State<MixnetContractCache>,
) -> FormattedResponse<MixnetContractCacheTimestampResponse> {
let output = output.get_output();
let timestamp = cache.cache_timestamp().await;
output.to_response(MixnetContractCacheTimestampResponse { timestamp })
}
/// Allow to request to refresh the cache of all mixnet nodes on the network.
/// Note that this endpoint enforces high global rate limiting and realistically
/// should not be used outside very special scenarios.
#[utoipa::path(
tag = "Utility",
post,
request_body = RefreshNodeStatusCacheRequestBody,
path = "/refresh-node-annotations-cache",
context_path = "/v1/utility",
responses(
(status = 200, content(
(RefreshNodeStatusCacheResponse = "application/json"),
(RefreshNodeStatusCacheResponse = "application/yaml"),
(RefreshNodeStatusCacheResponse = "application/bincode")
))
),
params(OutputParams),
security(
("auth_token" = [])
)
)]
async fn refresh_node_annotations_cache(
Query(output): Query<OutputParams>,
State(cache): State<NodeAnnotationsCache>,
) -> AxumResult<FormattedResponse<RefreshNodeStatusCacheResponse>> {
let output = output.get_output();
let now = OffsetDateTime::now_utc();
// max 1 refresh every 5min (TODO: make it configurable)
let cutoff = now - Duration::from_secs(5 * 60);
let last = cache.refresh_handle.last_requested();
if last > cutoff {
return Err(AxumErrorResponse::too_many(
"already refreshed contract cache in the last 5 minutes",
));
}
cache.refresh_handle.request_refresh(now);
Ok(output.to_response(RefreshNodeStatusCacheResponse { success: true }))
}
/// Return information on when the mixnet cache has last been refreshed.
#[utoipa::path(
tag = "Utility",
get,
path = "/node-annotations-cache-timestamp",
context_path = "/v1/utility",
responses(
(status = 200, content(
(NodeStatusCacheTimestampResponse = "application/json"),
(NodeStatusCacheTimestampResponse = "application/yaml"),
(NodeStatusCacheTimestampResponse = "application/bincode")
))
),
params(OutputParams),
security(
("auth_token" = [])
)
)]
async fn node_annotations_cache_timestamp(
Query(output): Query<OutputParams>,
State(cache): State<NodeStatusCache>,
) -> FormattedResponse<NodeStatusCacheTimestampResponse> {
let output = output.get_output();
let timestamp = cache.cache_timestamp().await;
output.to_response(NodeStatusCacheTimestampResponse { timestamp })
}
+9
View File
@@ -64,8 +64,13 @@ pub struct Debug {
/// of the services providers
pub minimum_mix_performance: u8,
/// Specifies the maximum time this node will wait for its initial valid topology
#[serde(with = "humantime_serde")]
pub maximum_initial_topology_waiting_time: Duration,
/// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process.
#[serde(alias = "maximum_auth_request_age")]
#[serde(with = "humantime_serde")]
pub max_request_timestamp_skew: Duration,
pub stale_messages: StaleMessageDebug,
@@ -85,6 +90,8 @@ impl Debug {
pub const DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW: Duration = Duration::from_secs(120);
pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192;
pub const DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK: Duration = Duration::from_secs(30);
pub const DEFAULT_MAXIMUM_INITIAL_TOPOLOGY_WAITING_TIME: Duration =
Duration::from_secs(15 * 60);
}
impl Default for Debug {
@@ -98,6 +105,8 @@ impl Default for Debug {
client_bandwidth: Default::default(),
zk_nym_tickets: Default::default(),
upgrade_mode_min_staleness_recheck: Self::DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK,
maximum_initial_topology_waiting_time:
Self::DEFAULT_MAXIMUM_INITIAL_TOPOLOGY_WAITING_TIME,
}
}
}
+1
View File
@@ -149,6 +149,7 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig {
debug: config.service_providers.ip_packet_router.debug.client_debug,
},
ip_packet_router: nym_ip_packet_router::config::IpPacketRouter {
open_proxy: config.service_providers.open_proxy,
disable_poisson_rate: config
.service_providers
.ip_packet_router
@@ -765,6 +765,10 @@ pub async fn try_upgrade_config_v12<P: AsRef<Path>>(
message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit,
maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections,
minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance,
// \/ ADDED
maximum_initial_topology_waiting_time: gateway_tasks::Debug::default()
.maximum_initial_topology_waiting_time,
// /\ ADDED
max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew,
stale_messages: StaleMessageDebug {
cleaner_run_interval: old_cfg
+3
View File
@@ -206,6 +206,9 @@ pub enum NymNodeError {
)]
InitialTopologyQueryFailure { source: ValidatorClientError },
#[error("failed to retrieve initial valid topology within the specified deadline")]
InitialTopologyTimeout,
#[error("experienced critical failure with the replay detection bloomfilter: {message}")]
BloomfilterFailure { message: &'static str },
+9 -2
View File
@@ -645,6 +645,11 @@ impl NymNode {
self.config.mixnet.nym_api_urls.clone(),
self.config.debug.topology_cache_ttl,
self.config.debug.routing_nodes_check_interval,
self.config
.gateway_tasks
.debug
.maximum_initial_topology_waiting_time,
self.config.gateway_tasks.debug.minimum_mix_performance,
self.shutdown_manager.clone_shutdown_token(),
)
.await
@@ -1384,8 +1389,11 @@ impl NymNode {
active_egress_mixnet_connections,
);
let network = network_refresher.cached_network();
network_refresher.start();
self.start_gateway_tasks(
network_refresher.cached_network(),
network,
lp_nodes,
metrics_sender,
active_clients_store,
@@ -1396,7 +1404,6 @@ impl NymNode {
self.setup_key_rotation(nym_apis_client, bloomfilters_manager)
.await?;
network_refresher.start();
self.shutdown_manager.close_tracker();
Ok(self.shutdown_manager)
+60 -26
View File
@@ -29,7 +29,7 @@ use std::ops::Deref;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::time::interval;
use tokio::time::{Instant, interval, sleep};
use tracing::log::error;
use tracing::{debug, trace, warn};
use url::Url;
@@ -149,27 +149,12 @@ impl CachedTopologyProvider {
#[async_trait]
impl TopologyProvider for CachedTopologyProvider {
async fn get_new_topology(&mut self) -> Option<NymTopology> {
let network_guard = self.cached_network.inner.read().await;
let self_node = self.gateway_node.identity_key;
let mut topology = NymTopology::new(
network_guard.topology_metadata,
network_guard.rewarded_set.clone(),
Vec::new(),
)
.with_additional_nodes(
network_guard
.network_nodes
.iter()
.map(|node| &node.basic)
.filter(|node| {
if node.supported_roles.mixnode {
node.performance.round_to_integer() >= self.min_mix_performance
} else {
true
}
}),
);
let mut topology = self
.cached_network
.network_topology(self.min_mix_performance)
.await;
if !topology.has_node(self.gateway_node.identity_key) {
debug!("{self_node} didn't exist in topology. inserting it.",);
@@ -196,6 +181,29 @@ impl CachedNetwork {
})),
}
}
async fn network_topology(&self, min_mix_performance: u8) -> NymTopology {
let network_guard = self.inner.read().await;
NymTopology::new(
network_guard.topology_metadata,
network_guard.rewarded_set.clone(),
Vec::new(),
)
.with_additional_nodes(
network_guard
.network_nodes
.iter()
.map(|node| &node.basic)
.filter(|node| {
if node.supported_roles.mixnode {
node.performance.round_to_integer() >= min_mix_performance
} else {
true
}
}),
)
}
}
struct CachedNetworkInner {
@@ -217,12 +225,15 @@ pub struct NetworkRefresher {
}
impl NetworkRefresher {
#[allow(clippy::too_many_arguments)]
pub(crate) async fn initialise_new(
testnet: bool,
user_agent: UserAgent,
nym_api_urls: Vec<Url>,
full_refresh_interval: Duration,
pending_check_interval: Duration,
max_startup_waiting_period: Duration,
min_mix_performance: u8,
shutdown_token: ShutdownToken,
) -> Result<Self, NymNodeError> {
let nym_api = nym_http_api_client::Client::builder(nym_api_urls[0].clone())?
@@ -245,7 +256,8 @@ impl NetworkRefresher {
lp_nodes: Default::default(),
};
this.obtain_initial_network().await?;
this.obtain_initial_network(max_startup_waiting_period, min_mix_performance)
.await?;
Ok(this)
}
@@ -322,7 +334,7 @@ impl NetworkRefresher {
self.routing_filter.resolved.swap_denied(current_denied);
self.routing_filter.pending.clear().await;
//update noise Noise Nodes
//update noise Nodes
let noise_nodes = nodes
.iter()
.filter(|n| n.x25519_noise_versioned_key.is_some())
@@ -359,10 +371,32 @@ impl NetworkRefresher {
}
}
pub(crate) async fn obtain_initial_network(&mut self) -> Result<(), NymNodeError> {
self.refresh_network_nodes_inner()
.await
.map_err(|source| NymNodeError::InitialTopologyQueryFailure { source })
pub(crate) async fn obtain_initial_network(
&mut self,
max_startup_waiting_period: Duration,
min_mix_performance: u8,
) -> Result<(), NymNodeError> {
// make it configurable
const STARTUP_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
let start = Instant::now();
loop {
self.refresh_network_nodes_inner()
.await
.map_err(|source| NymNodeError::InitialTopologyQueryFailure { source })?;
let topology = self.network.network_topology(min_mix_performance).await;
if topology.is_minimally_routable() {
return Ok(());
}
if start.elapsed() > max_startup_waiting_period {
return Err(NymNodeError::InitialTopologyTimeout);
}
sleep(STARTUP_REFRESH_INTERVAL).await;
}
}
pub(crate) fn routing_filter(&self) -> NetworkRoutingFilter {
+21 -2
View File
@@ -52,6 +52,7 @@ pub struct MixnetClientBuilder<S: MixnetClientStorage = Ephemeral> {
socks5_config: Option<Socks5>,
wait_for_gateway: bool,
wait_for_initial_topology: bool,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
custom_shutdown: Option<ShutdownTracker>,
@@ -94,6 +95,7 @@ impl MixnetClientBuilder<OnDiskPersistent> {
storage_paths: None,
socks5_config: None,
wait_for_gateway: false,
wait_for_initial_topology: false,
custom_topology_provider: None,
storage: storage_paths
.initialise_default_persistent_storage()
@@ -132,6 +134,7 @@ where
storage_paths: None,
socks5_config: None,
wait_for_gateway: false,
wait_for_initial_topology: false,
custom_topology_provider: None,
custom_gateway_transceiver: None,
custom_shutdown: None,
@@ -157,6 +160,7 @@ where
storage_paths: self.storage_paths,
socks5_config: self.socks5_config,
wait_for_gateway: self.wait_for_gateway,
wait_for_initial_topology: self.wait_for_initial_topology,
custom_topology_provider: self.custom_topology_provider,
custom_gateway_transceiver: self.custom_gateway_transceiver,
custom_shutdown: self.custom_shutdown,
@@ -293,13 +297,21 @@ where
self
}
/// Attempt to wait for the selected gateway (if applicable) to come online if its currently not bonded.
/// Attempt to wait for the selected gateway (if applicable) to come online if it's currently not bonded.
#[must_use]
pub fn with_wait_for_gateway(mut self, wait_for_gateway: bool) -> Self {
self.wait_for_gateway = wait_for_gateway;
self
}
/// Attempt to wait for initial network topology to become online before finalizing client setup
/// this is useful during network bootstrapping phases
#[must_use]
pub fn with_wait_for_initial_topology(mut self, wait_for_initial_topology: bool) -> Self {
self.wait_for_initial_topology = wait_for_initial_topology;
self
}
#[must_use]
pub fn with_user_agent(mut self, user_agent: UserAgent) -> Self {
self.user_agent = Some(user_agent);
@@ -352,6 +364,7 @@ where
client.custom_topology_provider = self.custom_topology_provider;
client.custom_shutdown = self.custom_shutdown;
client.wait_for_gateway = self.wait_for_gateway;
client.wait_for_initial_topology = self.wait_for_initial_topology;
client.force_tls = self.force_tls;
client.no_hostname = self.no_hostname;
client.user_agent = self.user_agent;
@@ -400,9 +413,13 @@ where
/// advanced usage of custom gateways
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
/// Attempt to wait for the selected gateway (if applicable) to come online if its currently not bonded.
/// Attempt to wait for the selected gateway (if applicable) to come online if it's currently not bonded.
wait_for_gateway: bool,
/// Attempt to wait for initial network topology to become online before finalizing client setup
/// this is useful during network bootstrapping phases
wait_for_initial_topology: bool,
/// Force the client to connect using wss protocol with the gateway.
force_tls: bool,
@@ -476,6 +493,7 @@ where
custom_topology_provider: None,
custom_gateway_transceiver: None,
wait_for_gateway: false,
wait_for_initial_topology: false,
force_tls: false,
no_hostname: false,
custom_shutdown: None,
@@ -765,6 +783,7 @@ where
let mut base_builder: BaseClientBuilder<_, _> =
BaseClientBuilder::new(base_config, self.storage, self.dkg_query_client)
.with_wait_for_gateway(self.wait_for_gateway)
.with_wait_for_initial_topology(self.wait_for_initial_topology)
.with_forget_me(&self.forget_me)
.with_remember_me(&self.remember_me)
.with_derivation_material(self.derivation_material);
@@ -188,6 +188,10 @@ impl Config {
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct IpPacketRouter {
/// specifies whether this IP Router should run in 'open-proxy' mode
/// and thus would attempt to resolve **ANY** request it receives.
pub open_proxy: bool,
/// Disable Poisson sending rate.
pub disable_poisson_rate: bool,
@@ -199,6 +203,7 @@ pub struct IpPacketRouter {
impl Default for IpPacketRouter {
fn default() -> Self {
IpPacketRouter {
open_proxy: false,
disable_poisson_rate: true,
#[allow(clippy::expect_used)]
upstream_exit_policy_url: Some(
@@ -94,6 +94,7 @@ impl Default for IpPacketRouterV1 {
impl From<IpPacketRouterV1> for IpPacketRouter {
fn from(value: IpPacketRouterV1) -> Self {
IpPacketRouter {
open_proxy: false,
disable_poisson_rate: value.disable_poisson_rate,
upstream_exit_policy_url: value.upstream_exit_policy_url,
}
@@ -37,6 +37,7 @@ pub struct IpPacketRouter {
config: Config,
wait_for_gateway: bool,
wait_for_topology: bool,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
shutdown: ShutdownTracker,
@@ -48,6 +49,7 @@ impl IpPacketRouter {
Self {
config,
wait_for_gateway: false,
wait_for_topology: false,
custom_topology_provider: None,
custom_gateway_transceiver: None,
shutdown,
@@ -72,6 +74,13 @@ impl IpPacketRouter {
self
}
#[must_use]
#[allow(unused)]
pub fn with_wait_for_initial_topology(mut self, wait_for_initial_topology: bool) -> Self {
self.wait_for_topology = wait_for_initial_topology;
self
}
#[must_use]
pub fn with_minimum_gateway_performance(mut self, minimum_gateway_performance: u8) -> Self {
self.config.base.debug.topology.minimum_gateway_performance = minimum_gateway_performance;
@@ -131,6 +140,7 @@ impl IpPacketRouter {
self.custom_gateway_transceiver,
self.custom_topology_provider,
self.wait_for_gateway,
self.wait_for_topology,
&self.config.storage_paths.common_paths,
)
.await?;
@@ -19,6 +19,7 @@ use crate::{
use super::ClientVersion;
#[derive(Debug)]
pub(crate) struct VersionedResponse {
pub(crate) version: ClientVersion,
pub(crate) reply_to: ConnectedClientId,
@@ -17,6 +17,7 @@ pub(crate) async fn create_mixnet_client(
custom_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
wait_for_gateway: bool,
wait_for_topology: bool,
paths: &CommonClientPaths,
) -> Result<nym_sdk::mixnet::MixnetClient, IpPacketRouterError> {
let debug_config = config.debug;
@@ -34,7 +35,8 @@ pub(crate) async fn create_mixnet_client(
.debug_config(debug_config)
.custom_shutdown(shutdown)
.with_user_agent(user_agent)
.with_wait_for_gateway(wait_for_gateway);
.with_wait_for_gateway(wait_for_gateway)
.with_wait_for_initial_topology(wait_for_topology);
if !config.get_disabled_credentials_mode() {
client_builder = client_builder.enable_credentials_mode();
}
@@ -35,6 +35,13 @@ impl ExitPolicyRequestFilter {
}
}
pub fn new_from_policy(policy: ExitPolicy) -> Self {
ExitPolicyRequestFilter {
upstream: None,
policy,
}
}
#[allow(unused)]
pub fn policy(&self) -> &ExitPolicy {
&self.policy
@@ -5,6 +5,7 @@ use crate::config::Config;
use crate::error::IpPacketRouterError;
use crate::request_filter::exit_policy::ExitPolicyRequestFilter;
use log::{info, warn};
use nym_exit_policy::ExitPolicy;
use std::{net::SocketAddr, sync::Arc};
pub mod exit_policy;
@@ -42,12 +43,17 @@ impl RequestFilter {
}
async fn new_exit_policy_filter(config: &Config) -> Result<Self, IpPacketRouterError> {
let upstream_url = config
.ip_packet_router
.upstream_exit_policy_url
.as_ref()
.ok_or(IpPacketRouterError::NoUpstreamExitPolicy)?;
let policy_filter = ExitPolicyRequestFilter::new_upstream(upstream_url.clone()).await?;
let policy_filter = if config.ip_packet_router.open_proxy {
ExitPolicyRequestFilter::new_from_policy(ExitPolicy::new_open())
} else {
let upstream_url = config
.ip_packet_router
.upstream_exit_policy_url
.as_ref()
.ok_or(IpPacketRouterError::NoUpstreamExitPolicy)?;
ExitPolicyRequestFilter::new_upstream(upstream_url.clone()).await?
};
Ok(RequestFilter {
inner: Arc::new(RequestFilterInner::ExitPolicy { policy_filter }),
})
@@ -64,6 +64,7 @@ pub struct NRServiceProviderBuilder {
config: Config,
wait_for_gateway: bool,
wait_for_topology: bool,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
shutdown: ShutdownTracker,
@@ -153,6 +154,7 @@ impl NRServiceProviderBuilder {
NRServiceProviderBuilder {
config,
wait_for_gateway: false,
wait_for_topology: false,
custom_topology_provider: None,
custom_gateway_transceiver: None,
shutdown,
@@ -181,6 +183,15 @@ impl NRServiceProviderBuilder {
self
}
#[must_use]
// this is a false positive, this method is actually called when used as a library
// but clippy complains about it when building the binary
#[allow(unused)]
pub fn with_wait_for_initial_topology(mut self, wait_for_initial_topology: bool) -> Self {
self.wait_for_topology = wait_for_initial_topology;
self
}
#[must_use]
// this is a false positive, this method is actually called when used as a library
// but clippy complains about it when building the binary
@@ -232,6 +243,7 @@ impl NRServiceProviderBuilder {
self.custom_gateway_transceiver,
self.custom_topology_provider,
self.wait_for_gateway,
self.wait_for_topology,
&self.config.storage_paths.common_paths,
)
.await?;
@@ -554,6 +566,7 @@ async fn create_mixnet_client(
custom_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
wait_for_gateway: bool,
wait_for_topology: bool,
paths: &CommonClientPaths,
) -> Result<nym_sdk::mixnet::MixnetClient, NetworkRequesterError> {
let debug_config = config.debug;
@@ -569,7 +582,8 @@ async fn create_mixnet_client(
.network_details(NymNetworkDetails::new_from_env())
.debug_config(debug_config)
.custom_shutdown(shutdown)
.with_wait_for_gateway(wait_for_gateway);
.with_wait_for_gateway(wait_for_gateway)
.with_wait_for_initial_topology(wait_for_topology);
if !config.get_disabled_credentials_mode() {
client_builder = client_builder.enable_credentials_mode();
}
@@ -1,5 +1,5 @@
[package]
name = "testnet-manager"
name = "localnet-orchestrator"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
@@ -8,39 +8,44 @@ documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = false
[dependencies]
anyhow.workspace = true
bip39.workspace = true
bs58.workspace = true
anyhow = { workspace = true }
bip39 = { workspace = true }
bytes = { workspace = true }
cargo-edit = { workspace = true }
cfg-if = { workspace = true }
console = { workspace = true }
cw-utils.workspace = true
cw-utils = { workspace = true }
clap = { workspace = true, features = ["cargo", "derive"] }
futures = { workspace = true }
indicatif = { workspace = true }
itertools = { workspace = true }
humantime = { workspace = true }
rand.workspace = true
rand = { workspace = true }
reqwest = { workspace = true, features = ["json", "stream", "rustls"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
serde_json = { workspace = true }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] }
strum_macros = { workspace = true }
tempfile = { workspace = true }
thiserror.workspace = true
time = { workspace = true, features = ["parsing", "formatting"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "process"] }
toml = { workspace = true }
tracing.workspace = true
url.workspace = true
tracing = { workspace = true }
url = { workspace = true }
zeroize = { workspace = true, features = ["zeroize_derive"] }
nym-bin-common = { workspace = true, features = ["output_format", "basic_tracing"] }
nym-crypto = { workspace = true, features = ["asymmetric", "rand", "serde"] }
nym-config = { workspace = true }
nym-validator-client = { workspace = true }
nym-validator-client = { workspace = true, features = ["http-client"] }
nym-compact-ecash = { workspace = true }
nym-http-api-client = { workspace = true }
dkg-bypass-contract = { path = "dkg-bypass-contract", default-features = false }
nym-pemstore = { workspace = true }
# contracts:
nym-mixnet-contract-common = { workspace = true }
@@ -51,10 +56,14 @@ nym-ecash-contract-common = { workspace = true }
nym-coconut-dkg-common = { workspace = true }
nym-multisig-contract-common = { workspace = true }
nym-performance-contract-common = { workspace = true }
nym-pemstore = { workspace = true }
dkg-bypass-contract = { path = "dkg-bypass-contract", default-features = false }
[build-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
[lints]
workspace = true
@@ -0,0 +1,3 @@
# Localnet Orchestrator
based off the testnet manager (to be deprecated)
@@ -5,7 +5,7 @@ use std::env;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/nym-api-example.sqlite");
let database_path = format!("{out_dir}/localnet-example.sqlite");
// remove the db file if it already existed from previous build
// in case it was from a different branch
@@ -22,13 +22,7 @@ async fn main() -> anyhow::Result<()> {
.await
.context("Failed to perform SQLx migrations")?;
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
#[cfg(target_family = "windows")]
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
Ok(())
}
@@ -7,7 +7,9 @@ use cosmwasm_std::{
Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdError, StdResult, Storage,
entry_point,
};
use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex};
use cw_storage_plus::{
Index, IndexList, IndexedMap, Item, Map, MultiIndex, SnapshotItem, Strategy,
};
use nym_coconut_dkg_common::dealer::DealerRegistrationDetails;
use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState, NodeIndex};
use nym_coconut_dkg_common::verification_key::ContractVKShare;
@@ -15,6 +17,18 @@ use nym_coconut_dkg_common::verification_key::ContractVKShare;
pub(crate) type Dealer<'a> = &'a Addr;
pub(crate) const CURRENT_EPOCH: Item<Epoch> = Item::new("current_epoch");
pub const HISTORICAL_EPOCH: SnapshotItem<Epoch> = SnapshotItem::new(
"historical_epoch",
"historical_epoch__checkpoints",
"historical_epoch__changelog",
Strategy::EveryBlock,
);
#[allow(deprecated)]
pub fn save_epoch(storage: &mut dyn Storage, height: u64, epoch: &Epoch) -> StdResult<()> {
CURRENT_EPOCH.save(storage, epoch)?;
HISTORICAL_EPOCH.save(storage, epoch, height)
}
pub const THRESHOLD: Item<u64> = Item::new("threshold");
@@ -104,8 +118,9 @@ pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result<Response,
.time_configuration
.state_duration(EpochState::InProgress);
CURRENT_EPOCH.save(
save_epoch(
deps.storage,
0,
&Epoch {
state: EpochState::InProgress,
epoch_id: 0,
@@ -0,0 +1,98 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
CREATE TABLE account
(
address TEXT NOT NULL PRIMARY KEY UNIQUE,
mnemonic TEXT NOT NULL
);
CREATE TABLE nyxd
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
rpc_endpoint TEXT NOT NULL,
master_address TEXT NOT NULL REFERENCES account (address)
);
CREATE table nym_api
(
network_id INTEGER NOT NULL PRIMARY KEY REFERENCES localnet_metadata (id),
endpoint TEXT NOT NULL
);
CREATE TABLE contract
(
-- note: I'm purposely not using contract address as primary key,
-- as you can have the same addresses for different contracts (on different instances of localnets)
-- as addressing is semi-kinda deterministic-ish
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
address TEXT NOT NULL,
admin_address TEXT NOT NULL REFERENCES account (address)
);
CREATE TABLE localnet_metadata
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-- human-readable name associated with the localnet (to have some unique prefix for containers)
name TEXT NOT NULL UNIQUE,
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE localnet_contracts
(
metadata_id INTEGER NOT NULL PRIMARY KEY REFERENCES localnet_metadata (id),
mixnet_contract_id INTEGER NOT NULL REFERENCES contract (id),
vesting_contract_id INTEGER NOT NULL REFERENCES contract (id),
ecash_contract_id INTEGER NOT NULL REFERENCES contract (id),
cw3_multisig_contract_id INTEGER NOT NULL REFERENCES contract (id),
cw4_group_contract_id INTEGER NOT NULL REFERENCES contract (id),
dkg_contract_id INTEGER NOT NULL REFERENCES contract (id),
performance_contract_id INTEGER NOT NULL REFERENCES contract (id)
);
CREATE TABLE localnet_auxiliary_accounts
(
network_id INTEGER NOT NULL PRIMARY KEY REFERENCES localnet_metadata (id),
rewarder_address TEXT NOT NULL REFERENCES account (address),
ecash_holding_account_address TEXT NOT NULL REFERENCES account (address)
);
CREATE TABLE localnet
(
metadata_id INTEGER NOT NULL PRIMARY KEY REFERENCES localnet_metadata (id),
nyxd_id INTEGER NOT NULL REFERENCES nyxd (id)
);
-- keep it separate to more easily support testing having multiple network monitors
CREATE TABLE authorised_network_monitor
(
address TEXT NOT NULL PRIMARY KEY REFERENCES account (address),
network_id INTEGER NOT NULL REFERENCES localnet_metadata (id)
);
CREATE TABLE metadata
(
id INTEGER PRIMARY KEY CHECK (id = 0),
latest_network_id INTEGER REFERENCES localnet_metadata (id),
latest_nyxd_id INTEGER REFERENCES nyxd (id)
);
CREATE TABLE nym_node
(
node_id INTEGER NOT NULL,
identity_key TEXT NOT NULL PRIMARY KEY,
private_identity_key TEXT NOT NULL,
network_id INTEGER NOT NULL REFERENCES localnet_metadata (id),
owner_address TEXT NOT NULL REFERENCES account (address),
gateway BOOL NOT NULL
);
INSERT OR IGNORE INTO metadata(id)
VALUES (0);
@@ -0,0 +1,221 @@
## Localnet:
Result of marrying the dedicated `localnet.sh` script and the old `Testnet Manager`.
It allows to run a complete Nym mixnet test environment on Apple's `container` runtime or on Linux `containerd` (via
`nerdctl` and kata shim).
It results in creation of the following containers:
- `nyxd`
- `nym-api`
- `nym-node-1` (gateway)
- `nym-node-2` (mixnode)
- `nym-node-3` (mixnode)
- `nym-node-4` (mixnode)
which run on a custom brige network (`nym-localnet-network`) with dynamic IP assignment:
```
Host Machine (macOS)
├── nym-localnet-network (bridge)
│ ├── nyxd (192.168.66.3)
│ ├── nym-api (192.168.66.4)
│ ├── nym-node-1 (192.168.66.5)
│ ├── nym-node-2 (192.168.66.6)
│ ├── nym-node-3 (192.168.66.7)
│ └── nym-node-4 (192.168.66.8)
```
it also embeddeds `nym-gateway-probe` binary in the container image for easy testing.
### Prerequisites
#### MacOS
- **MUST** have MacOS Tahoe for inter-container networking
- `brew install --cask container`
- Download Kata Containers 3.20, this one can be loaded by `container` and has `CONFIG_TUN=y` kernel flag
- `https://github.com/kata-containers/kata-containers/releases/download/3.20.0/kata-static-3.20.0-arm64.tar.xz`
- Load new kernel
-
`container system kernel set --tar kata-static-3.20.0-arm64.tar.xz --binary opt/kata/share/kata-containers/vmlinux-6.12.42-162`
- Validate kernel version once you have container running
#### Linux
The following dependencies must be installed:
- `newuidmap` and `newgidmap` which can be installed via `uidmap` package
- `containerd` which will probably come with your distro
- `nerdctl`, `kata-runtime` and `containerd-shim-kata-v2`. they can be either installed manually or via
`kata-manager.sh` script: https://github.com/kata-containers/kata-containers/blob/main/utils/README.md#kata-manager.
it is recommended to run it with the `-N` flag to install it alongside `nerdctl`
### Quick Start
```bash
# navigate to the root of the nym monorepo
# (exact command will depend on the relative location of the directory on your machine)
cd nym
# build the orchestrator binary
cargo run --release --bin localnet-orchestrator
# run the orchestrator binary to startup the network
target/release/nym-localnet-orchestrator up
# run the gateway probe test
target/release/nym-localnet-orchestrator run-gateway-probe-test
# purge all the containers and build data
target/release/nym-localnet-orchestrator purge
```
### Startup flow
The startup is separated into 4 main steps (which can also be run individually as separate commands)
1. `initialise-nyxd`
- builds `nyxd` docker image from https://github.com/nymtech/nyxd.git and imports it into the `container` runtime
- initialises the `genesis.json` of the localnet chain and saves it to a shared volume
- starts up `nyxd` container using the shared volume data
2. `initialise-contracts`
- either downloads nym contracts or builds all of them fresh using `cosmwasm/optimizer` image
- uploads and initialises all the contracts onto the chain
- fixes up state inconsistencies (the bootstrap problem) by performing additional contract migrations
3. `initialise-nym-api`
- builds `nym-binaries` docker image and imports it into the `container` runtime. note: its version tag is based on
the current version of the `nym-node` binary
- generates DKG keys to allow future zk-nym issuance and injects those into a shared volume to be used by the
`nym-api`
- initialises `nym-api` data and starts its container using a shared volume
- overwrites the states of the `dkg` and `group` contracts by forcing the just created `nym-api` instance to be a
valid zk-nym issuer
4. `initialise-nym-nodes`
- initialises data of 4 nym-nodes `nym-node --init-only`: 3 mixnodes and 1 gateway
- bonds all of them into previously created mixnet contract
- force assigns them to the active set by performing additional admin-only contract shenanigans
- force refreshes nym-api caches to make the nodes appear in the relevant endpoints immediately
- injects fake "100%" network monitor scores for each node in the `nym-api` container to make sure all nodes have
valid performance metrics and force refreshes the relevant cache
### Commands
#### `build-info`
Show build information of the localnet orchestrator binary
#### `initialise-nyxd`
Initialise new nyxd instance as described above
##### Relevant arguments:
- `nyxd-tag` to allow using non-default nyxd repo branch
#### `initialise-contracts`
Upload and initialise all Nym cosmwasm contracts as described above
##### Relevant arguments:
- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root
- `reproducible-builds` - ensure contract builds are fully reproducible by removing additional source of
non-determinism. note that this slows down the build process significantly
- `ci-build-branch` - use prebuilt contracts from the `build.ci.nymte.ch` server
- `cosmwasm-optimizer-image` - cosmwasm optimizer image used for building and optimising the contracts
- `allow-cached-build` - allow using pre-built contracts from previous localnet runs
#### `initialise-nym-api`
Initialise instance of nym api and adjust the DKG contract to allow it to immediately start issuing zk-nyms as described
above
##### Relevant arguments:
- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root
- `cosmwasm-optimizer-image` - cosmwasm optimizer image used for building and optimising the contracts
- `allow-cached-build` - allow using pre-built contracts from previous localnet runs
- `custom-dns` - allows specifying custom nameserver to be used by all spawned containers
#### `initialise-nym-nodes`
Initialise nym nodes to start serving mixnet (and wireguard) traffic. this involves bonding them in the contract and
starting the containers as described above
##### Relevant arguments:
- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root
- `open-proxy` - allow internal service providers to run in open proxy mode
- `custom-dns` - allows specifying custom nameserver to be used by all spawned containers
#### `run-gateway-probe-test`
Run a gateway probe against the running localnet
##### Relevant arguments:
- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root
- `prove-args` - allows specifying additional flags to be passed to the gateway probe
#### `rebuild-binaries-image`
Rebuild the docker and container image used for running the nym binaries
##### Relevant arguments:
- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root
- `custom-tag` - custom image tag for the new image
#### `up`
Single command to start up localnet with minimal configuration
##### Relevant arguments:
refer to arguments of `initialise-nyxd`, `initialise-contracts`, `initialise-nym-api` and `initialise-nym-nodes` as the
same ones are available
#### `down`
Stop the localnet (stops and removes all containers using `localnet-*` image
#### `purge`
Remove all localnet information, including any containers and images
##### Relevant arguments:
- `monorepo-root` - specify path to the monorepo root if the current working directory is different from the root
- `remove-cache` (default: true) - specify whether the cache data should be removed
- `remove-images` (default: true) - specify whether the built images should be removed
### Storage
All the localnet data is saved, by default, under `~/.nym/localnet-orchestrator/` directory and further split into the
following:
- `network-data.sqlite` (by default `~/.nym/localnet-orchestrator/network-data.sqlite`) which contains basic network
metadata - it was easier than jugling random .json files around
- each container has its volume stored in:
- $NETWORK_NAME/nym-api (e.g. `~/.nym/localnet-orchestrator/group-key/nym-api`)
- $NETWORK_NAME/nyxd (e.g. `~/.nym/localnet-orchestrator/group-key/nyxd`)
- $NETWORK_NAME/nym-node-1 (e.g. `~/.nym/localnet-orchestrator/group-key/nym-node-1`)
- $NETWORK_NAME/nym-node-2 (e.g. `~/.nym/localnet-orchestrator/group-key/nym-node-2`)
- $NETWORK_NAME/nym-node-3 (e.g. `~/.nym/localnet-orchestrator/group-key/nym-node-3`)
- $NETWORK_NAME/nym-node-4 (e.g. `~/.nym/localnet-orchestrator/group-key/nym-node-4`)
- `~/.nym/localnet-orchestrator/.cache` which contains intermediate build data that can be reused between runs to speed
up the deployment process. currently it only contains `contracts` directory for built cosmwasm contracts
### Current Limitations:
- `nyxd` instance exposes port `26657` to the host. this was to speed up development to allow easier chain interaction
by being able to use rust client directly from the orchestrator host. in the future this should get modified
- no windows support
- no docker compose - custom orchestrator is used instead
- dynamic ips - container ip addresses may change between restarts, thus there's a lot of inflexibility with a network
setup. once created it cannot be modified
@@ -1,9 +1,9 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::NetworkManagerError;
use nym_bin_common::bin_info_owned;
use nym_bin_common::output_format::OutputFormat;
use tracing::debug;
#[derive(clap::Args, Debug)]
pub(crate) struct Args {
@@ -11,7 +11,8 @@ pub(crate) struct Args {
output: OutputFormat,
}
pub(crate) fn execute(args: Args) -> Result<(), NetworkManagerError> {
pub(crate) fn execute(args: Args) -> anyhow::Result<()> {
debug!("args: {args:#?}");
println!("{}", args.output.format(&bin_info_owned!()));
Ok(())
}
@@ -0,0 +1,20 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::CommonArgs;
use crate::orchestrator::LocalnetOrchestrator;
use tracing::debug;
#[derive(clap::Args, Debug)]
pub(crate) struct Args {
#[clap(flatten)]
common: CommonArgs,
}
pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
debug!("args: {args:#?}");
// during the initial setup the prerequisites are checked
LocalnetOrchestrator::new(&args.common).await?;
Ok(())
}
@@ -0,0 +1,21 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::CommonArgs;
use crate::orchestrator::LocalnetOrchestrator;
use tracing::debug;
#[derive(clap::Args, Debug)]
pub(crate) struct Args {
#[clap(flatten)]
common: CommonArgs,
}
pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
debug!("args: {args:#?}");
LocalnetOrchestrator::new(&args.common)
.await?
.stop_localnet()
.await
}
@@ -0,0 +1,79 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::CommonArgs;
use crate::orchestrator::LocalnetOrchestrator;
use crate::orchestrator::setup::cosmwasm_contracts;
use crate::orchestrator::state::LocalnetState;
use anyhow::bail;
use nym_bin_common::output_format::OutputFormat;
use std::path::PathBuf;
use tracing::debug;
#[derive(clap::Args, Debug)]
#[clap(group(clap::ArgGroup::new("built-contracts").required(false)))]
pub(crate) struct Args {
#[clap(flatten)]
common: CommonArgs,
/// Point to on-disk director containing .wasm files of all required Nym contracts
#[clap(long, group = "built-contracts")]
contracts_directory: Option<PathBuf>,
/// Provide a branch name to be used for attempting to retrieve .wasm files from the ci build server,
/// e.g. for branch `feature/my-amazing-feature`, the following urls will be used:
/// - `https://builds.ci.nymte.ch/feature/my-amazing-feature/mixnet_contract.wasm`
/// - `https://builds.ci.nymte.ch/feature/my-amazing-feature/nym_performance_contract.wasm`
/// - ...
/// - etc.
#[clap(long, group = "built-contracts")]
ci_build_branch: Option<String>,
/// Ensure contracts wasm code is fully reproducible by building those them
/// with linux/amd64 platform and forcing some additional cargo build flags.
/// Note: it will cause significant (build-time) overhead for M1 Macs
#[clap(long)]
reproducible_builds: bool,
/// Cosmwasm optimizer image used for building and optimising the contracts
#[clap(long, default_value = "cosmwasm/optimizer:0.17.0")]
cosmwasm_optimizer_image: String,
/// Custom path to root of the monorepo in case this binary has been executed from a different location.
/// If not provided, it is going to get assumed that the current directory is the monorepo root
#[clap(long)]
monorepo_root: Option<PathBuf>,
/// Specify whether the orchestrator can attempt to retrieve previously built cached contracts.
#[clap(long, conflicts_with = "reproducible_builds")]
allow_cached_build: bool,
#[clap(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
debug!("args: {args:#?}");
let mut orchestrator = LocalnetOrchestrator::new(&args.common).await?;
if orchestrator.state != LocalnetState::RunningNyxd {
bail!(
"can't initialise cosmwasm contracts - nyxd is not running or the contracts have already been initialised. the localnet is in {} state.",
orchestrator.state
)
}
orchestrator
.initialise_contracts(cosmwasm_contracts::Config {
reproducible_builds: args.reproducible_builds,
cosmwasm_optimizer_image: args.cosmwasm_optimizer_image,
explicit_contracts_directory: args.contracts_directory,
ci_build_branch: args.ci_build_branch,
monorepo_root: args.monorepo_root,
allow_cached_build: args.allow_cached_build,
})
.await?;
Ok(())
}
@@ -0,0 +1,57 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::CommonArgs;
use crate::orchestrator::LocalnetOrchestrator;
use crate::orchestrator::setup::nym_api;
use crate::orchestrator::state::LocalnetState;
use anyhow::bail;
use nym_bin_common::output_format::OutputFormat;
use std::path::PathBuf;
use tracing::debug;
#[derive(clap::Args, Debug)]
pub(crate) struct Args {
#[clap(flatten)]
common: CommonArgs,
/// Cosmwasm optimizer image used for building and optimising the contracts
#[clap(long, default_value = "cosmwasm/optimizer:0.17.0")]
cosmwasm_optimizer_image: String,
/// Custom path to root of the monorepo in case this binary has been executed from a different location.
/// If not provided, it is going to get assumed that the current directory is the monorepo root
#[clap(long)]
monorepo_root: Option<PathBuf>,
/// Specify whether the orchestrator can attempt to retrieve previously built cached contracts.
#[clap(long)]
allow_cached_build: bool,
#[clap(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
debug!("args: {args:#?}");
let mut orchestrator = LocalnetOrchestrator::new(&args.common).await?;
if orchestrator.state != LocalnetState::DeployedNymContracts {
bail!(
"can't initialise nym api - nym contracts have not already been initialised or nym api is already running. the localnet is in {} state.",
orchestrator.state
)
}
orchestrator
.initialise_nym_api(nym_api::Config {
cosmwasm_optimizer_image: args.cosmwasm_optimizer_image,
monorepo_root: args.monorepo_root,
custom_dns: args.common.custom_dns,
allow_cached_build: args.allow_cached_build,
})
.await?;
Ok(())
}
@@ -0,0 +1,52 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::CommonArgs;
use crate::orchestrator::LocalnetOrchestrator;
use crate::orchestrator::setup::nym_nodes;
use crate::orchestrator::state::LocalnetState;
use anyhow::bail;
use nym_bin_common::output_format::OutputFormat;
use std::path::PathBuf;
use tracing::debug;
#[derive(clap::Args, Debug)]
pub(crate) struct Args {
#[clap(flatten)]
common: CommonArgs,
/// Custom path to root of the monorepo in case this binary has been executed from a different location.
/// If not provided, it is going to get assumed that the current directory is the monorepo root
#[clap(long)]
monorepo_root: Option<PathBuf>,
/// Specify whether internal service providers should run in open proxy mode
#[clap(long)]
open_proxy: bool,
#[clap(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
debug!("args: {args:#?}");
let mut orchestrator = LocalnetOrchestrator::new(&args.common).await?;
if orchestrator.state != LocalnetState::RunningNymApi {
bail!(
"can't initialise nym nodes - nym api has not already been initialised or nym nodes are already running. the localnet is in {} state.",
orchestrator.state
)
}
orchestrator
.initialise_nym_nodes(nym_nodes::Config {
monorepo_root: args.monorepo_root,
custom_dns: args.common.custom_dns,
open_proxy: args.open_proxy,
})
.await?;
Ok(())
}
@@ -0,0 +1,53 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::CommonArgs;
use crate::orchestrator::LocalnetOrchestrator;
use crate::orchestrator::setup::nyxd;
use crate::orchestrator::state::LocalnetState;
use anyhow::bail;
use nym_bin_common::output_format::OutputFormat;
use tracing::debug;
use url::Url;
#[derive(clap::Args, Debug)]
pub(crate) struct Args {
#[clap(flatten)]
common: CommonArgs,
#[clap(long, default_value = "https://github.com/nymtech/nyxd.git")]
nyxd_repo: Url,
/// Absolute path (from the repo root) to the location of the Dockerfile used for building nyxd
#[clap(long, default_value = "Dockerfile.dev")]
nyxd_dockerfile_path: String,
#[clap(long, default_value = "v0.60.1")]
nyxd_tag: String,
#[clap(short, long, default_value_t = OutputFormat::default())]
output: OutputFormat,
}
pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
debug!("args: {args:#?}");
let mut orchestrator = LocalnetOrchestrator::new(&args.common).await?;
if orchestrator.state != LocalnetState::Uninitialised {
bail!(
"can't initialise nyxd as it appears to have already been initialised. the localnet is in {} state.",
orchestrator.state
)
}
orchestrator
.initialise_nyxd(nyxd::Config {
nyxd_repo: args.nyxd_repo,
nyxd_dockerfile_path: args.nyxd_dockerfile_path,
custom_dns: args.common.custom_dns,
nyxd_tag: args.nyxd_tag,
})
.await?;
Ok(())
}
@@ -0,0 +1,122 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::{Parser, Subcommand};
use nym_bin_common::bin_info;
use std::path::PathBuf;
use std::sync::OnceLock;
pub(crate) mod build_info;
pub(crate) mod check_prerequisites;
pub(crate) mod down;
pub(crate) mod initialise_contracts;
pub(crate) mod initialise_nym_api;
pub(crate) mod initialise_nym_nodes;
pub(crate) mod initialise_nyxd;
pub(crate) mod purge;
pub(crate) mod rebuild_binaries_image;
pub(crate) mod run_gateway_probe_test;
pub(crate) mod up;
#[derive(clap::Args, Debug)]
pub(crate) struct CommonArgs {
#[clap(long, group = "storage")]
pub(crate) localnet_storage_path: Option<PathBuf>,
#[clap(long)]
pub(crate) orchestrator_db: Option<PathBuf>,
#[clap(long)]
pub(crate) existing_network: Option<String>,
/// Custom DNS flag ('--dns') to pass to all spawned containers
#[clap(long)]
pub(crate) custom_dns: Option<String>,
/// Specify whether all the data should be cleaned-up after use
#[clap(long, group = "storage")]
pub(crate) ephemeral: bool,
}
impl CommonArgs {
//
}
fn pretty_build_info_static() -> &'static str {
static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new();
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
}
#[derive(Parser, Debug)]
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
pub(crate) struct Cli {
#[clap(subcommand)]
command: Commands,
}
impl Cli {
pub(crate) async fn execute(self) -> anyhow::Result<()> {
match self.command {
Commands::BuildInfo(args) => build_info::execute(args),
Commands::InitialiseNyxd(args) => initialise_nyxd::execute(args).await,
Commands::InitialiseContracts(args) => initialise_contracts::execute(args).await,
Commands::InitialiseNymApi(args) => initialise_nym_api::execute(args).await,
Commands::InitialiseNymNodes(args) => initialise_nym_nodes::execute(args).await,
Commands::RunGatewayProbeTest(args) => run_gateway_probe_test::execute(args).await,
Commands::RebuildBinariesImage(args) => rebuild_binaries_image::execute(args).await,
Commands::CheckPrerequisites(args) => check_prerequisites::execute(args).await,
Commands::Up(args) => up::execute(args).await,
Commands::Down(args) => down::execute(args).await,
Commands::Purge(args) => purge::execute(args).await,
}
}
}
#[derive(Subcommand, Debug)]
pub(crate) enum Commands {
/// Show build information of this binary
BuildInfo(build_info::Args),
/// Initialise new nyxd instance
InitialiseNyxd(initialise_nyxd::Args),
/// Upload and initialise all Nym cosmwasm contracts
InitialiseContracts(initialise_contracts::Args),
/// Initialise instance of nym api and adjust the DKG contract
/// to allow it to immediately start issuing zk-nyms
InitialiseNymApi(initialise_nym_api::Args),
/// Initialise nym nodes to start serving mixnet (and wireguard) traffic.
/// this involves bonding them in the contract and starting the containers
InitialiseNymNodes(initialise_nym_nodes::Args),
/// Run a gateway probe against the running localnet
RunGatewayProbeTest(run_gateway_probe_test::Args),
/// Rebuild the docker and container image used for running the nym binaries
RebuildBinariesImage(rebuild_binaries_image::Args),
/// Performs basic prerequisites check for running the orchestrator
CheckPrerequisites(check_prerequisites::Args),
/// Single command to start up localnet with minimal configuration
Up(up::Args),
/// Stop the localnet (stops and removes all containers using `localnet-*` image
Down(down::Args),
/// Remove all localnet information, including any containers and images
Purge(purge::Args),
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}
@@ -0,0 +1,41 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::cli::CommonArgs;
use crate::orchestrator::LocalnetOrchestrator;
use crate::orchestrator::setup::purge;
use clap::ArgAction;
use std::path::PathBuf;
use tracing::debug;
#[derive(clap::Args, Debug)]
pub(crate) struct Args {
#[clap(flatten)]
common: CommonArgs,
/// Remove any built docker and container images
#[clap(long, action = ArgAction::Set, default_value_t = true)]
remove_images: bool,
/// Remove any cached build data
#[clap(long, action = ArgAction::Set, default_value_t = true)]
remove_cache: bool,
/// Custom path to root of the monorepo in case this binary has been executed from a different location.
/// If not provided, it is going to get assumed that the current directory is the monorepo root
#[clap(long)]
monorepo_root: Option<PathBuf>,
}
pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
debug!("args: {args:#?}");
LocalnetOrchestrator::new(&args.common)
.await?
.purge_localnet(purge::Config {
remove_images: args.remove_images,
remove_cache: args.remove_cache,
monorepo_root: args.monorepo_root,
})
.await
}

Some files were not shown because too many files have changed in this diff Show More