From ab3b3dd36982ddaedd3249b01a94ca9ae72cd8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 9 Feb 2023 13:58:14 +0100 Subject: [PATCH] rust-sdk: restructure API to builder pattern --- Cargo.toml | 5 +- .../client-core/src/client/base_client/mod.rs | 12 + .../reply_storage/backend/browser_backend.rs | 11 + .../reply_storage/backend/fs_backend/mod.rs | 7 + .../replies/reply_storage/backend/mod.rs | 11 +- .../client-libs/gateway-client/src/client.rs | 1 + .../manually_handle_keys_and_config.rs | 57 ++- sdk/rust/nym-sdk/examples/persist_keys.rs | 39 -- sdk/rust/nym-sdk/examples/simple.rs | 2 +- .../examples/simple_but_manually_connect.rs | 30 -- sdk/rust/nym-sdk/src/error.rs | 2 + sdk/rust/nym-sdk/src/mixnet.rs | 12 +- sdk/rust/nym-sdk/src/mixnet/client.rs | 368 +++++++++++------- sdk/rust/nym-sdk/src/mixnet/paths.rs | 17 + .../common/examples/control_requests.rs | 2 +- 15 files changed, 337 insertions(+), 239 deletions(-) delete mode 100644 sdk/rust/nym-sdk/examples/persist_keys.rs delete mode 100644 sdk/rust/nym-sdk/examples/simple_but_manually_connect.rs diff --git a/Cargo.toml b/Cargo.toml index 0e4a5cf2c21..bff66fddb4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,8 +107,11 @@ edition = "2021" [workspace.dependencies] async-trait = "0.1.63" +lazy_static = "1.4.0" log = "0.4" -thiserror = "1.0.38" serde = "1.0.152" serde_json = "1.0.91" +tap = "1.0.1" +thiserror = "1.0.38" tokio = "1.24.1" +url = "2.2" diff --git a/clients/client-core/src/client/base_client/mod.rs b/clients/client-core/src/client/base_client/mod.rs index 67d76614349..e6316942fcd 100644 --- a/clients/client-core/src/client/base_client/mod.rs +++ b/clients/client-core/src/client/base_client/mod.rs @@ -51,11 +51,22 @@ pub mod non_wasm_helpers; pub mod helpers; +#[derive(Clone)] pub struct ClientInput { pub connection_command_sender: ConnectionCommandSender, pub input_sender: InputMessageSender, } +impl ClientInput { + pub async fn send( + &self, + message: InputMessage, + ) -> Result<(), tokio::sync::mpsc::error::SendError> { + self.input_sender.send(message).await + } +} + +#[derive(Clone)] pub struct ClientOutput { pub received_buffer_request_sender: ReceivedBufferRequestSender, } @@ -303,6 +314,7 @@ where let shared_key = if self.key_manager.is_gateway_key_set() { Some(self.key_manager.gateway_shared_key()) } else { + log::info!("Gateway key not set! Will proceed anyway."); None }; diff --git a/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs b/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs index 1d37ddb0435..4aa9dee63aa 100644 --- a/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs +++ b/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs @@ -5,6 +5,8 @@ use crate::client::replies::reply_storage::backend::Empty; use crate::client::replies::reply_storage::{CombinedReplyStorage, ReplyStorageBackend}; use async_trait::async_trait; +use std::path::PathBuf; + // well, right now we don't have the browser storage : ( // so we keep everything in memory #[derive(Debug)] @@ -27,6 +29,15 @@ impl Backend { impl ReplyStorageBackend for Backend { type StorageError = ::StorageError; + async fn new(debug_config: &crate::config::DebugConfig, _db_path: Option) -> Self { + Backend { + empty: Empty { + min_surb_threshold: debug_config.minimum_reply_surb_storage_threshold, + max_surb_threshold: debug_config.maximum_reply_surb_storage_threshold, + }, + } + } + async fn flush_surb_storage( &mut self, storage: &CombinedReplyStorage, diff --git a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs index 931c2f925fc..ce75b6afcdd 100644 --- a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs +++ b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::client::base_client::non_wasm_helpers; use crate::client::replies::reply_storage::backend::fs_backend::manager::StorageManager; use crate::client::replies::reply_storage::backend::fs_backend::models::{ ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender, @@ -367,6 +368,12 @@ impl Backend { impl ReplyStorageBackend for Backend { type StorageError = error::StorageError; + async fn new(debug_config: &crate::config::DebugConfig, db_path: Option) -> Self { + non_wasm_helpers::setup_fs_reply_surb_backend(db_path, debug_config) + .await + .unwrap() + } + fn is_active(&self) -> bool { self.manager.is_active() } diff --git a/clients/client-core/src/client/replies/reply_storage/backend/mod.rs b/clients/client-core/src/client/replies/reply_storage/backend/mod.rs index ea26bc13524..e1727795d09 100644 --- a/clients/client-core/src/client/replies/reply_storage/backend/mod.rs +++ b/clients/client-core/src/client/replies/reply_storage/backend/mod.rs @@ -3,7 +3,7 @@ use crate::client::replies::reply_storage::CombinedReplyStorage; use async_trait::async_trait; -use std::error::Error; +use std::{error::Error, path::PathBuf}; use thiserror::Error; #[cfg(target_arch = "wasm32")] @@ -30,6 +30,13 @@ pub struct Empty { impl ReplyStorageBackend for Empty { type StorageError = UndefinedError; + async fn new(debug_config: &crate::config::DebugConfig, _db_path: Option) -> Self { + Self { + min_surb_threshold: debug_config.minimum_reply_surb_storage_threshold, + max_surb_threshold: debug_config.maximum_reply_surb_storage_threshold, + } + } + async fn flush_surb_storage( &mut self, _storage: &CombinedReplyStorage, @@ -63,6 +70,8 @@ impl ReplyStorageBackend for Empty { pub trait ReplyStorageBackend: Sized { type StorageError: Error + 'static; + async fn new(debug_config: &crate::config::DebugConfig, db_path: Option) -> Self; + fn is_active(&self) -> bool { true } diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index 8ecaa532de6..e88a08de536 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -441,6 +441,7 @@ where } debug_assert!(self.connection.is_available()); + log::trace!("Registering gateway"); // it's fine to instantiate it here as it's only used once (during authentication or registration) // and putting it into the GatewayClient struct would be a hassle diff --git a/sdk/rust/nym-sdk/examples/manually_handle_keys_and_config.rs b/sdk/rust/nym-sdk/examples/manually_handle_keys_and_config.rs index 95b76fd75e7..d099529364e 100644 --- a/sdk/rust/nym-sdk/examples/manually_handle_keys_and_config.rs +++ b/sdk/rust/nym-sdk/examples/manually_handle_keys_and_config.rs @@ -4,41 +4,62 @@ use nym_sdk::mixnet; async fn main() { logging::setup_logging(); - // We can set a few options let user_chosen_gateway_id = None; let nym_api_endpoints = vec!["https://validator.nymtech.net/api/".parse().unwrap()]; - let config = mixnet::Config::new(user_chosen_gateway_id, nym_api_endpoints); - let mut client = mixnet::MixnetClient::builder(Some(config), None) - .await - .unwrap(); - // Just some plain data to pretend we have some external storage that the application // implementer is using. let mut mock_storage = MockStorage::empty(); - // In this we want to provide our own gateway config struct, and handle persisting this info to disk - // ourselves (e.g., as part of our own configuration file). let first_run = true; - if first_run { - client.register_with_gateway().await.unwrap(); + + let client = if first_run { + // Create a client without a storage backend + let mut client = mixnet::MixnetClientBuilder::new() + .config(config) + .build::() + .await + .unwrap(); + + // In this we want to provide our own gateway config struct, and handle persisting this info to disk + // ourselves (e.g., as part of our own configuration file). + client.register_and_authenticate_gateway().await.unwrap(); mock_storage.write(client.get_keys(), client.get_gateway_endpoint().unwrap()); + client } else { let (keys, gateway_config) = mock_storage.read(); - client.set_keys(keys); - client.set_gateway_endpoint(gateway_config); - } + + // Create a client without a storage backend, but with explicitly set keys and gateway + // configuration. This creates the client in a registered state. + let client = mixnet::MixnetClientBuilder::new() + .config(config) + .keys(keys) + .gateway_config(gateway_config) + .build::() + .await + .unwrap(); + client + }; // Connect to the mixnet, now we're listening for incoming - let client = client.connect_to_mixnet().await.unwrap(); + let mut client = client.connect_to_mixnet().await.unwrap(); // Be able to get our client address - println!("Our client address is {}", client.nym_address()); + let our_address = client.nym_address(); + println!("Our client nym address is: {our_address}"); // Send important info up the pipe to a buddy - let recipient = mixnet::Recipient::try_from_base58_string("foo.bar@blah").unwrap(); - client.send_str(recipient, "flappappa").await; + client.send_str(*our_address, "hello there").await; + + println!("Waiting for message"); + if let Some(received) = client.wait_for_messages().await { + for r in received { + println!("Received: {}", String::from_utf8_lossy(&r.message)); + } + } + + client.disconnect().await; } #[allow(unused)] @@ -53,7 +74,7 @@ impl MockStorage { } fn write(&mut self, _keys: mixnet::KeysArc, _gateway_config: &mixnet::GatewayEndpointConfig) { - todo!(); + log::info!("todo"); } fn empty() -> Self { diff --git a/sdk/rust/nym-sdk/examples/persist_keys.rs b/sdk/rust/nym-sdk/examples/persist_keys.rs deleted file mode 100644 index dc983e695ac..00000000000 --- a/sdk/rust/nym-sdk/examples/persist_keys.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::path::PathBuf; - -use nym_sdk::mixnet; - -#[tokio::main] -async fn main() { - logging::setup_logging(); - - // Specify some config options - let config_dir = PathBuf::from("/tmp/mixnet-client"); - - // Setting `KeyMode::Keep` will use existing keys, and existing config, if there is one. - // Regardles of `user_chosen_gateway`. - let keys = mixnet::StoragePaths::new_from_dir(mixnet::KeyMode::Keep, &config_dir).unwrap(); - - // Provide key paths for the client to read/write keys to. - let client = mixnet::MixnetClient::builder(None, Some(keys)) - .await - .unwrap(); - - // Connect to the mixnet, now we're listening for incoming - let mut client = client.connect_to_mixnet().await.unwrap(); - - // Be able to get our client address - let our_address = client.nym_address(); - println!("Our client nym address is: {our_address}"); - - // Send a message throught the mixnet to ourselves - client.send_str(*our_address, "hello there").await; - - println!("Waiting for message"); - if let Some(received) = client.wait_for_messages().await { - for r in received { - println!("Received: {}", String::from_utf8_lossy(&r.message)); - } - } - - client.disconnect().await; -} diff --git a/sdk/rust/nym-sdk/examples/simple.rs b/sdk/rust/nym-sdk/examples/simple.rs index a485dbf75e2..73d4aba8215 100644 --- a/sdk/rust/nym-sdk/examples/simple.rs +++ b/sdk/rust/nym-sdk/examples/simple.rs @@ -5,7 +5,7 @@ async fn main() { logging::setup_logging(); // Passing no config makes the client fire up an ephemeral session and figure shit out on its own - let mut client = mixnet::MixnetClient::connect().await.unwrap(); + let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); // Be able to get our client address let our_address = client.nym_address(); diff --git a/sdk/rust/nym-sdk/examples/simple_but_manually_connect.rs b/sdk/rust/nym-sdk/examples/simple_but_manually_connect.rs deleted file mode 100644 index 5e9a98fa7e4..00000000000 --- a/sdk/rust/nym-sdk/examples/simple_but_manually_connect.rs +++ /dev/null @@ -1,30 +0,0 @@ -use nym_sdk::mixnet; - -#[tokio::main] -async fn main() { - logging::setup_logging(); - - // Create client builder, including ephemeral keys. The builder can be usable in the context - // where you don't want to connect just yet. - // Since not storage paths are given, the surb storage will be inactive. - let client = mixnet::MixnetClient::builder(None, None).await.unwrap(); - - // Now we connect to the mixnet, using ephemeral keys already created - let mut client = client.connect_to_mixnet().await.unwrap(); - - // Be able to get our client address - let our_address = client.nym_address(); - println!("Our client nym address is: {our_address}"); - - // Send a message throught the mixnet to ourselves - client.send_str(*our_address, "hello there").await; - - println!("Waiting for message"); - if let Some(received) = client.wait_for_messages().await { - for r in received { - println!("Received: {}", String::from_utf8_lossy(&r.message)); - } - } - - client.disconnect().await; -} diff --git a/sdk/rust/nym-sdk/src/error.rs b/sdk/rust/nym-sdk/src/error.rs index 5e835db5401..c801fc121f2 100644 --- a/sdk/rust/nym-sdk/src/error.rs +++ b/sdk/rust/nym-sdk/src/error.rs @@ -31,6 +31,8 @@ pub enum Error { supported, and likely and user mistake" )] ReregisteringGatewayNotSupported, + #[error("no gateway key set")] + NoGatewayKeySet, } pub type Result = std::result::Result; diff --git a/sdk/rust/nym-sdk/src/mixnet.rs b/sdk/rust/nym-sdk/src/mixnet.rs index c4c4164958e..4c63c6a5e69 100644 --- a/sdk/rust/nym-sdk/src/mixnet.rs +++ b/sdk/rust/nym-sdk/src/mixnet.rs @@ -10,7 +10,7 @@ //! async fn main() { //! // Passing no config makes the client fire up an ephemeral session and figure stuff out on //! // its own -//! let mut client = mixnet::MixnetClient::connect().await.unwrap(); +//! let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); //! //! // Be able to get our client address //! let our_address = client.nym_address(); @@ -36,8 +36,14 @@ mod connection_state; mod keys; mod paths; -pub use client::{MixnetClient, MixnetClientBuilder}; -pub use client_core::config::GatewayEndpointConfig; +pub use client::{DisconnectedMixnetClient, MixnetClient, MixnetClientBuilder, MixnetClientSender}; +pub use client_core::{ + client::{ + inbound_messages::InputMessage, + replies::reply_storage::{fs_backend::Backend as ReplyStorage, Empty as EmptyReplyStorage}, + }, + config::GatewayEndpointConfig, +}; pub use config::Config; pub use keys::{Keys, KeysArc}; pub use nymsphinx::{ diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index c38ad4bea82..61e70175d06 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -4,13 +4,12 @@ use client_connections::TransmissionLane; use client_core::{ client::{ base_client::{ - helpers::setup_empty_reply_surb_backend, non_wasm_helpers, BaseClientBuilder, - ClientInput, ClientOutput, ClientState, CredentialsToggle, + BaseClientBuilder, ClientInput, ClientOutput, ClientState, CredentialsToggle, }, inbound_messages::InputMessage, key_manager::KeyManager, received_buffer::ReconstructedMessagesReceiver, - replies::reply_storage::{self, ReplyStorageBackend}, + replies::reply_storage::ReplyStorageBackend, }, config::{persistence::key_pathfinder::ClientKeyPathfinder, GatewayEndpointConfig}, }; @@ -28,13 +27,75 @@ use crate::{Error, Result}; use super::{connection_state::BuilderState, Config, GatewayKeyMode, Keys, KeysArc, StoragePaths}; +#[derive(Default)] +pub struct MixnetClientBuilder { + config: Option, + storage_paths: Option, + keys: Option, + gateway_config: Option, +} + +impl MixnetClientBuilder { + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn config(mut self, config: Config) -> Self { + self.config = Some(config); + self + } + + /// Enabled storage + #[must_use] + pub fn enable_storage(mut self, paths: StoragePaths) -> Self { + self.storage_paths = Some(paths); + self + } + + #[must_use] + pub fn keys(mut self, keys: Keys) -> Self { + self.keys = Some(keys); + self + } + + #[must_use] + pub fn gateway_config(mut self, gateway_config: GatewayEndpointConfig) -> Self { + self.gateway_config = Some(gateway_config); + self + } + + /// Construct a [`DisconnectedMixnetClient`] from the setup specified. + pub async fn build(self) -> Result> + where + B: ReplyStorageBackend + Send + Sync + 'static, + { + let config = self.config.unwrap_or_default(); + let storage_paths = self.storage_paths; + + let mut client = DisconnectedMixnetClient::new(Some(config), storage_paths).await; + + if let Some(keys) = self.keys { + client.set_keys(keys); + } + + // If we have a gateway config, we can move the client into a registered state. This will + // fail if no gateway key is set. + if let Some(gateway_config) = self.gateway_config { + client.register_gateway_with_config(gateway_config)?; + } + + Ok(client) + } +} + /// Represents a client that is not yet connected to the mixnet. You typically create one when you /// want to have a separate configuration and connection phase. Once the mixnet client builder is /// configured, call [`MixnetClientBuilder::connect_to_mixnet()`] to transition to a connected /// client. -pub struct MixnetClientBuilder +pub struct DisconnectedMixnetClient where - B: ReplyStorageBackend, + B: ReplyStorageBackend + Sync + Send + 'static, { /// Keys handled by the client key_manager: KeyManager, @@ -53,10 +114,24 @@ where reply_storage_backend: B, } -impl MixnetClientBuilder +impl DisconnectedMixnetClient where B: ReplyStorageBackend + Sync + Send + 'static, { + async fn new( + config: Option, + paths: Option, + ) -> DisconnectedMixnetClient { + let config = config.unwrap_or_default(); + + let reply_surb_database_path = paths.as_ref().map(|p| p.reply_surb_database_path.clone()); + + let reply_storage_backend = + ReplyStorageBackend::new(&config.debug_config, reply_surb_database_path).await; + + create_new_client_with_custom_storage(Some(config), paths, reply_storage_backend).unwrap() + } + /// Client keys are generated at client creation if none were found. The gateway shared /// key, however, is created during the gateway registration handshake so it might not /// necessarily be available. @@ -65,7 +140,7 @@ where } /// Sets the keys of this [`MixnetClientBuilder`]. - pub fn set_keys(&mut self, keys: Keys) { + fn set_keys(&mut self, keys: Keys) { self.key_manager.set_identity_keypair(keys.identity_keypair); self.key_manager .set_encryption_keypair(keys.encryption_keypair); @@ -75,22 +150,29 @@ where .insert_gateway_shared_key(Arc::new(keys.gateway_shared_key)); } - /// Returns the keys of this [`MixnetClientBuilder`]. Client keys are always available since - /// if none are specified at creation time, new random ones are generated. + /// Returns the keys of this [`DisconnectedMixnetClient`]. Client keys are always available + /// since if none are specified at creation time, new random ones are generated. pub fn get_keys(&self) -> KeysArc { KeysArc::from(&self.key_manager) } /// Sets the gateway endpoint of this [`MixnetClientBuilder`]. - pub fn set_gateway_endpoint(&mut self, gateway_endpoint_config: GatewayEndpointConfig) { + /// + /// NOTE: this will mark this builder as `Registered`, and the it is assumed that the keys are + /// also explicitly set. + pub fn register_gateway_with_config( + &mut self, + gateway_endpoint_config: GatewayEndpointConfig, + ) -> Result<()> { + if !self.has_gateway_key() { + return Err(Error::NoGatewayKeySet); + } + self.state = BuilderState::Registered { gateway_endpoint_config, - } - } + }; - /// Returns the get gateway endpoint of this [`MixnetClientBuilder`]. - pub fn get_gateway_endpoint(&self) -> Option<&GatewayEndpointConfig> { - self.state.gateway_endpoint_config() + Ok(()) } /// Register with a gateway. If a gateway is provided in the config then that will try to be @@ -100,10 +182,11 @@ where /// /// This function will return an error if you try to re-register when in an already registered /// state. - pub async fn register_with_gateway(&mut self) -> Result<()> { + pub async fn register_and_authenticate_gateway(&mut self) -> Result<()> { if self.state != BuilderState::New { return Err(Error::ReregisteringGatewayNotSupported); } + log::debug!("Registering with gateway"); let user_chosen_gateway = self .config @@ -125,6 +208,11 @@ where Ok(()) } + /// Returns the get gateway endpoint of this [`MixnetClientBuilder`]. + pub fn get_gateway_endpoint(&self) -> Option<&GatewayEndpointConfig> { + self.state.gateway_endpoint_config() + } + fn write_gateway_key(&self, paths: StoragePaths, key_mode: &GatewayKeyMode) -> Result<()> { let path_finder = ClientKeyPathfinder::from(paths); if path_finder.gateway_key_file_exists() && key_mode.is_keep() { @@ -175,7 +263,10 @@ where /// /// #[tokio::main] /// async fn main() { - /// let client = mixnet::MixnetClient::builder(None, None).await.unwrap(); + /// let client = mixnet::MixnetClientBuilder::new() + /// .build::() + /// .await + /// .unwrap(); /// let client = client.connect_to_mixnet().await.unwrap(); /// } /// ``` @@ -197,16 +288,21 @@ where // If we didn't find any shared gateway key during creation, that means we first // need to register a gateway log::trace!("Gateway key NOT found: registering new"); - self.register_with_gateway().await?; + self.register_and_authenticate_gateway().await?; self.write_gateway_key(paths.clone(), &GatewayKeyMode::Overwrite)?; self.write_gateway_endpoint_config(&paths.gateway_endpoint_config)?; } } else { // If we don't have any key paths, just use ephemeral keys - self.register_with_gateway().await?; + self.register_and_authenticate_gateway().await?; } } + // If the gateway is in a registered state, but without the gateway key set. + if matches!(self.state, BuilderState::Registered { .. }) && !self.has_gateway_key() { + return Err(Error::NoGatewayKeySet); + } + // At this point we should be in a registered state, either at function entry or by the // above convenience logic. let BuilderState::Registered { gateway_endpoint_config } = self.state else { @@ -249,6 +345,70 @@ where } } +/// Create a new mixnet client builder. If no config options are supplied, creates a new client with +/// ephemeral keys stored in RAM, which will be discarded at application close. +/// +/// Callers have the option of supplying futher parameters to store persistent identities at a +/// location on-disk, if desired. +/// +/// A custom storage backend can be passed in. +/// +/// NOTE: the major reason for this being a free function is to allow convenient type deduction +fn create_new_client_with_custom_storage( + config_option: Option, + paths: Option, + reply_storage_backend: B, +) -> Result> +where + B: ReplyStorageBackend + Sync + Send + 'static, +{ + let config = config_option.unwrap_or_default(); + + // If we are provided paths to keys, use them if they are available. And if they are + // not, write the generated keys back to storage. + let key_manager = if let Some(ref paths) = paths { + let path_finder = ClientKeyPathfinder::from(paths.clone()); + + // Try load keys + match KeyManager::load_keys_but_gateway_is_optional(&path_finder) { + Ok(key_manager) => { + log::debug!("Keys loaded"); + key_manager + } + Err(err) => { + log::debug!("Not loading keys: {err}"); + if let Some(path) = path_finder.any_file_exists_and_return() { + if paths.operating_mode.is_keep() { + return Err(Error::DontOverwrite(path)); + } + } + + // Double check using a function that has slightly different internal logic. I + // know this is a bit defensive, but I don't want to overwrite + assert!(!(path_finder.any_file_exists() && paths.operating_mode.is_keep())); + + // Create new keys and write to storage + let key_manager = client_core::init::new_client_keys(); + // WARN: this will overwrite! + key_manager.store_keys(&path_finder)?; + key_manager + } + } + } else { + // Ephemeral keys that we only store in memory + log::debug!("Creating new ephemeral keys"); + client_core::init::new_client_keys() + }; + + Ok(DisconnectedMixnetClient { + key_manager, + config, + storage_paths: paths, + state: BuilderState::New, + reply_storage_backend, + }) +} + /// Client connected to the Nym mixnet. pub struct MixnetClient { /// The nym address of this connected client. @@ -268,7 +428,6 @@ pub struct MixnetClient { /// The current state of the client that is exposed to the user. This includes things like /// current message send queue length. - #[allow(dead_code)] client_state: ClientState, /// A channel for messages arriving from the mixnet after they have been reconstructed. @@ -289,125 +448,16 @@ impl MixnetClient { /// /// #[tokio::main] /// async fn main() { - /// let mut client = mixnet::MixnetClient::connect().await; - /// } - /// - /// ``` - pub async fn connect() -> Result { - let client = MixnetClient::builder_without_storage(None, None)?; - client.connect_to_mixnet().await - } - - /// Create a new mixnet client builder. If no config options are supplied, creates a new client - /// with ephemeral keys stored in RAM, which will be discarded at application close. - /// - /// Callers have the option of supplying futher parameters to store persistent identities at a - /// location on-disk, if desired. - /// - /// # Examples - /// - /// ```no_run - /// use nym_sdk::mixnet; - /// - /// #[tokio::main] - /// async fn main() { - /// let client = mixnet::MixnetClient::builder(None, None).await; + /// let mut client = mixnet::MixnetClient::connect_new().await; /// } - /// ``` - pub async fn builder( - config: Option, - paths: Option, - ) -> Result> { - let config = config.unwrap_or_default(); - - let reply_surb_database_path = paths.as_ref().map(|p| p.reply_surb_database_path.clone()); - - let reply_storage_backend = non_wasm_helpers::setup_fs_reply_surb_backend( - reply_surb_database_path, - &config.debug_config, - ) - .await?; - - MixnetClient::builder_with_custom_storage(Some(config), paths, reply_storage_backend) - } - - /// Create a new mixnet client builder. If no config options are supplied, creates a new client with - /// ephemeral keys stored in RAM, which will be discarded at application close. /// - /// Callers have the option of supplying futher parameters to store persistent identities at a - /// location on-disk, if desired. - /// - /// # Examples - /// - /// ``` - /// use nym_sdk::mixnet; - /// let client = mixnet::MixnetClient::builder_without_storage(None, None); /// ``` - pub fn builder_without_storage( - config: Option, - paths: Option, - ) -> Result> { - let config = config.unwrap_or_default(); - let reply_storage_backend = setup_empty_reply_surb_backend(&config.debug_config); - MixnetClient::builder_with_custom_storage(Some(config), paths, reply_storage_backend) - } - - /// Create a new mixnet client builder. If no config options are supplied, creates a new client with - /// ephemeral keys stored in RAM, which will be discarded at application close. - /// - /// Callers have the option of supplying futher parameters to store persistent identities at a - /// location on-disk, if desired. - /// - /// A custom storage backend can be passed in. - pub fn builder_with_custom_storage( - config_option: Option, - paths: Option, - reply_storage_backend: B, - ) -> Result> - where - B: ReplyStorageBackend, - { - let config = config_option.unwrap_or_default(); - - // If we are provided paths to keys, use them if they are available. And if they are - // not, write the generated keys back to storage. - let key_manager = if let Some(ref paths) = paths { - let path_finder = ClientKeyPathfinder::from(paths.clone()); - - // Try load keys - match KeyManager::load_keys_but_gateway_is_optional(&path_finder) { - Ok(key_manager) => key_manager, - Err(err) => { - log::debug!("Not loading keys: {err}"); - if let Some(path) = path_finder.any_file_exists_and_return() { - if paths.operating_mode.is_keep() { - return Err(Error::DontOverwrite(path)); - } - } - - // Double check using a function that has slightly different internal logic. I - // know this is a bit defensive, but I don't want to overwrite - assert!(!(path_finder.any_file_exists() && paths.operating_mode.is_keep())); - - // Create new keys and write to storage - let key_manager = client_core::init::new_client_keys(); - // WARN: this will overwrite! - key_manager.store_keys(&path_finder)?; - key_manager - } - } - } else { - // Ephemeral keys that we only store in memory - client_core::init::new_client_keys() - }; - - Ok(MixnetClientBuilder { - key_manager, - config, - storage_paths: paths, - state: BuilderState::New, - reply_storage_backend, - }) + pub async fn connect_new() -> Result { + MixnetClientBuilder::new() + .build::() + .await? + .connect_to_mixnet() + .await } /// Get the client identity, which is the public key of the identity key pair. @@ -421,6 +471,23 @@ impl MixnetClient { &self.nym_address } + /// Get a shallow clone of [`MixnetClientSender`] + pub fn sender(&self) -> MixnetClientSender { + MixnetClientSender { + client_input: self.client_input.clone(), + } + } + + /// Get a shallow clone of [`ConnectionCommandSender`]. + pub fn connection_command_sender(&self) -> client_connections::ConnectionCommandSender { + self.client_input.connection_command_sender.clone() + } + + /// Get a shallow clone of [`LaneQueueLengths`]. + pub fn shared_lane_queue_lengths(&self) -> client_connections::LaneQueueLengths { + self.client_state.shared_lane_queue_lengths.clone() + } + /// Sends stringy data to the supplied Nym address pub async fn send_str(&self, address: Recipient, message: &str) { let message_bytes = message.to_string().into_bytes(); @@ -430,7 +497,7 @@ impl MixnetClient { /// Sends stringy data to the supplied Nym address, and skip sending reply-SURBs pub async fn send_str_direct(&self, address: Recipient, message: &str) { let message_bytes = message.to_string().into_bytes(); - self.send_bytes(address, message_bytes).await; + self.send_bytes_direct(address, message_bytes).await; } /// Sends bytes to the supplied Nym address @@ -444,20 +511,19 @@ impl MixnetClient { /// async fn main() { /// let address = "foobar"; /// let recipient = mixnet::Recipient::try_from_base58_string(address).unwrap(); - /// let mut client = mixnet::MixnetClient::connect().await.unwrap(); + /// let mut client = mixnet::MixnetClient::connect_new().await.unwrap(); /// client.send_bytes(recipient, "hi".to_owned().into_bytes()).await; /// } /// ``` pub async fn send_bytes(&self, address: Recipient, message: Vec) { let lane = TransmissionLane::General; let input_msg = InputMessage::new_anonymous(address, message, 20, lane); - if self - .client_input - .input_sender - .send(input_msg) - .await - .is_err() - { + self.send_input_message(input_msg).await + } + + /// Sends a [`InputMessage`] to the mixnet. + async fn send_input_message(&self, message: InputMessage) { + if self.client_input.send(message).await.is_err() { log::error!("Failed to send message"); } } @@ -501,3 +567,15 @@ impl MixnetClient { self.task_manager.wait_for_shutdown().await; } } + +pub struct MixnetClientSender { + client_input: ClientInput, +} + +impl MixnetClientSender { + pub async fn send_input_message(&mut self, message: InputMessage) { + if self.client_input.send(message).await.is_err() { + log::error!("Failed to send message"); + } + } +} diff --git a/sdk/rust/nym-sdk/src/mixnet/paths.rs b/sdk/rust/nym-sdk/src/mixnet/paths.rs index ccf0da26409..dead111fdd4 100644 --- a/sdk/rust/nym-sdk/src/mixnet/paths.rs +++ b/sdk/rust/nym-sdk/src/mixnet/paths.rs @@ -104,3 +104,20 @@ impl From for ClientKeyPathfinder { } } } + +impl From<&client_core::config::Config> for StoragePaths { + fn from(value: &client_core::config::Config) -> Self { + Self { + operating_mode: KeyMode::Keep, + private_identity: value.get_private_identity_key_file(), + public_identity: value.get_public_identity_key_file(), + private_encryption: value.get_private_encryption_key_file(), + public_encryption: value.get_public_encryption_key_file(), + ack_key: value.get_ack_key_file(), + gateway_shared_key: value.get_gateway_shared_key_file(), + gateway_endpoint_config: Default::default(), + credential_database_path: value.get_database_path(), + reply_surb_database_path: value.get_reply_surb_database_path(), + } + } +} diff --git a/service-providers/common/examples/control_requests.rs b/service-providers/common/examples/control_requests.rs index 2a800ea7d3d..09f5823a3b9 100644 --- a/service-providers/common/examples/control_requests.rs +++ b/service-providers/common/examples/control_requests.rs @@ -33,7 +33,7 @@ async fn main() -> anyhow::Result<()> { // technically we don't need to start the entire client with all the subroutines, // but I needed an easy way of sending to and receiving from the mixnet // and that was the most straightforward way of achieving it - let mut client = MixnetClient::connect().await.unwrap(); + let mut client = MixnetClient::connect_new().await.unwrap(); let provider: Recipient = "AN8eLxYWFitCkMn92zim3PrPszxJZDYyFFKP7qnnAAew.8UAxL3LwQBis6WpM3GGXaqKGaVdnLCpGJWumHT6KNdTH@77TSuVU8d1oXKbPzjec2xh4i3Wj5WwUyy9Lr36sm8gZm".parse().unwrap(); // generic service provider request, so we don't even need to care it's to the socks5 provider