From 4f7b1c78e852e9ce0fc84ed30f85967cf5190a9d Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 30 Jul 2024 00:16:47 +0000 Subject: [PATCH 1/3] Move `tor::Client` setup into `crate::remote` --- src/commands/balance.rs | 15 +++++---------- src/remote.rs | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/commands/balance.rs b/src/commands/balance.rs index 52c139a..9c51413 100644 --- a/src/commands/balance.rs +++ b/src/commands/balance.rs @@ -1,5 +1,3 @@ -use std::path::Path; - use anyhow::anyhow; use gumdrop::Options; @@ -11,8 +9,9 @@ use zcash_client_sqlite::WalletDb; use zcash_protocol::value::{Zatoshis, COIN}; use crate::{ - data::{get_db_paths, get_tor_dir, get_wallet_network}, + data::{get_db_paths, get_wallet_network}, error, + remote::tor_client, ui::format_zec, MIN_CONFIRMATIONS, }; @@ -40,7 +39,8 @@ impl Command { .ok_or(error::Error::InvalidRecipient)?; let printer = if let Some(currency) = self.convert { - ValuePrinter::with_exchange_rate(&get_tor_dir(wallet_dir), currency).await? + let tor = tor_client(wallet_dir.as_ref()).await?; + ValuePrinter::with_exchange_rate(&tor, currency).await? } else { ValuePrinter::ZecOnly }; @@ -88,12 +88,7 @@ enum ValuePrinter { } impl ValuePrinter { - async fn with_exchange_rate(tor_dir: &Path, currency: Currency) -> anyhow::Result { - // Ensure Tor directory exists. - tokio::fs::create_dir_all(tor_dir).await?; - - let tor = tor::Client::create(tor_dir).await?; - + async fn with_exchange_rate(tor: &tor::Client, currency: Currency) -> anyhow::Result { info!("Fetching {:?}/ZEC exchange rate", currency); let exchanges = tor::http::cryptex::Exchanges::unauthenticated_known_with_gemini_trusted(); let usd_zec = tor.get_latest_zec_to_usd_rate(&exchanges).await?; diff --git a/src/remote.rs b/src/remote.rs index ce5abcf..7085c62 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -1,12 +1,16 @@ -use std::{borrow::Cow, fmt}; +use std::{borrow::Cow, fmt, path::Path}; use anyhow::anyhow; use tonic::transport::{Channel, ClientTlsConfig}; use tracing::info; -use zcash_client_backend::proto::service::compact_tx_streamer_client::CompactTxStreamerClient; +use zcash_client_backend::{ + proto::service::compact_tx_streamer_client::CompactTxStreamerClient, tor, +}; use zcash_protocol::consensus::Network; +use crate::data::get_tor_dir; + const ECC_TESTNET: &[Server<'_>] = &[Server::fixed("lightwalletd.testnet.electriccoin.co", 9067)]; const YWALLET_MAINNET: &[Server<'_>] = &[ @@ -151,3 +155,14 @@ pub(crate) async fn connect_to_lightwalletd( Ok(CompactTxStreamerClient::new(channel.connect().await?)) } + +pub(crate) async fn tor_client>( + wallet_dir: Option

, +) -> anyhow::Result { + let tor_dir = get_tor_dir(wallet_dir); + + // Ensure Tor directory exists. + tokio::fs::create_dir_all(&tor_dir).await?; + + Ok(tor::Client::create(&tor_dir).await?) +} From 93e8c44d833c1e8a8573b1b6ff737479b36c08b6 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 30 Jul 2024 00:28:36 +0000 Subject: [PATCH 2/3] Move `connect_to_lightwalletd` to `Server::connect_direct` --- src/commands/enhance.rs | 4 ++-- src/commands/import_ufvk.rs | 8 ++------ src/commands/init.rs | 4 ++-- src/commands/reset.rs | 4 ++-- src/commands/send.rs | 4 ++-- src/commands/sync.rs | 4 ++-- src/remote.rs | 28 +++++++++++++--------------- 7 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/commands/enhance.rs b/src/commands/enhance.rs index fb9e66d..f97c6ac 100644 --- a/src/commands/enhance.rs +++ b/src/commands/enhance.rs @@ -21,7 +21,7 @@ use zcash_protocol::consensus::{BlockHeight, BranchId, Network}; use crate::{ data::{get_db_paths, get_wallet_network}, - remote::{connect_to_lightwalletd, Servers}, + remote::Servers, }; // Options accepted for the `enhance` command @@ -89,7 +89,7 @@ impl Command { anyhow!("Chain height must be available to perform transaction enhancement.") })?; - let mut client = connect_to_lightwalletd(self.server.pick(params)?).await?; + let mut client = self.server.pick(params)?.connect_direct().await?; let mut satisfied_requests = BTreeSet::new(); loop { diff --git a/src/commands/import_ufvk.rs b/src/commands/import_ufvk.rs index 5fc81d8..946de70 100644 --- a/src/commands/import_ufvk.rs +++ b/src/commands/import_ufvk.rs @@ -10,11 +10,7 @@ use zcash_client_sqlite::WalletDb; use zcash_keys::keys::UnifiedFullViewingKey; use zcash_primitives::consensus; -use crate::{ - data::get_db_paths, - error, - remote::{connect_to_lightwalletd, Servers}, -}; +use crate::{data::get_db_paths, error, remote::Servers}; // Options accepted for the `import-ufvk` command #[derive(Debug, Options)] @@ -56,7 +52,7 @@ impl Command { let birthday = { // Fetch the tree state corresponding to the last block prior to the wallet's // birthday height. NOTE: THIS APPROACH LEAKS THE BIRTHDAY TO THE SERVER! - let mut client = connect_to_lightwalletd(self.server.pick(params)?).await?; + let mut client = self.server.pick(params)?.connect_direct().await?; let request = service::BlockId { height: (self.birthday - 1).into(), ..Default::default() diff --git a/src/commands/init.rs b/src/commands/init.rs index 4e558b5..0812256 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -15,7 +15,7 @@ use zcash_primitives::consensus::{self, Parameters}; use crate::{ data::{get_db_paths, init_wallet_config, Network}, error, - remote::{connect_to_lightwalletd, Servers}, + remote::Servers, }; // Options accepted for the `init` command @@ -50,7 +50,7 @@ impl Command { let params = consensus::Network::from(opts.network); // Get the current chain height (for the wallet's birthday). - let mut client = connect_to_lightwalletd(opts.server.pick(params)?).await?; + let mut client = opts.server.pick(params)?.connect_direct().await?; let birthday = if let Some(birthday) = opts.birthday { birthday } else { diff --git a/src/commands/reset.rs b/src/commands/reset.rs index 4493439..111cab4 100644 --- a/src/commands/reset.rs +++ b/src/commands/reset.rs @@ -3,7 +3,7 @@ use gumdrop::Options; use crate::{ data::{erase_wallet_state, read_config}, - remote::{connect_to_lightwalletd, Servers}, + remote::Servers, }; // Options accepted for the `reset` command @@ -27,7 +27,7 @@ impl Command { let params = keys.network(); // Connect to the client (for re-initializing the wallet). - let client = connect_to_lightwalletd(self.server.pick(params)?).await?; + let client = self.server.pick(params)?.connect_direct().await?; // Erase the wallet state (excluding key material). erase_wallet_state(wallet_dir.as_ref()).await; diff --git a/src/commands/send.rs b/src/commands/send.rs index 2714bfc..c3b724b 100644 --- a/src/commands/send.rs +++ b/src/commands/send.rs @@ -26,7 +26,7 @@ use crate::{ commands::propose::{parse_fee_rule, FeeRule}, data::{get_db_paths, read_config}, error, - remote::{connect_to_lightwalletd, Servers}, + remote::Servers, MIN_CONFIRMATIONS, }; @@ -87,7 +87,7 @@ impl Command { ) .map_err(error::Error::from)?; - let mut client = connect_to_lightwalletd(self.server.pick(params)?).await?; + let mut client = self.server.pick(params)?.connect_direct().await?; // Create the transaction. println!("Creating transaction..."); diff --git a/src/commands/sync.rs b/src/commands/sync.rs index 9804894..98d9c34 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -27,7 +27,7 @@ use zcash_protocol::consensus::{BlockHeight, Parameters}; use crate::{ data::{get_block_path, get_db_paths, get_wallet_network}, error, - remote::{connect_to_lightwalletd, Servers}, + remote::Servers, ShutdownListener, }; @@ -80,7 +80,7 @@ impl Command { let fsblockdb_root = fsblockdb_root.as_path(); let mut db_cache = FsBlockDb::for_path(fsblockdb_root).map_err(error::Error::from)?; let mut db_data = WalletDb::for_path(db_data, params)?; - let mut client = connect_to_lightwalletd(self.server.pick(params)?).await?; + let mut client = self.server.pick(params)?.connect_direct().await?; #[cfg(any(feature = "transparent-inputs", feature = "tui"))] let wallet_birthday = db_data diff --git a/src/remote.rs b/src/remote.rs index 7085c62..5b2d2eb 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -135,25 +135,23 @@ impl<'a> Server<'a> { self.port ) } -} -pub(crate) async fn connect_to_lightwalletd( - server: &Server<'_>, -) -> Result, anyhow::Error> { - info!("Connecting to {}", server); + pub(crate) async fn connect_direct(&self) -> anyhow::Result> { + info!("Connecting to {}", self); - let channel = Channel::from_shared(server.endpoint())?; + let channel = Channel::from_shared(self.endpoint())?; - let channel = if server.use_tls() { - let tls = ClientTlsConfig::new() - .domain_name(server.host.to_string()) - .with_webpki_roots(); - channel.tls_config(tls)? - } else { - channel - }; + let channel = if self.use_tls() { + let tls = ClientTlsConfig::new() + .domain_name(self.host.to_string()) + .with_webpki_roots(); + channel.tls_config(tls)? + } else { + channel + }; - Ok(CompactTxStreamerClient::new(channel.connect().await?)) + Ok(CompactTxStreamerClient::new(channel.connect().await?)) + } } pub(crate) async fn tor_client>( From d3e51e06d0733e2db7c49cdf061d7473e2617962 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 30 Jul 2024 00:47:06 +0000 Subject: [PATCH 3/3] Connect to non-local `lightwalletd` over Tor for everything except sync Most commands are single-shot, only making a couple of correlated requests to the server. Enhancement makes requests that could be made uncorrelated; that is left as a TODO. Syncing currently requires a long-running connection, so we don't migrate it yet. --- src/commands/enhance.rs | 13 +++++++++++-- src/commands/import_ufvk.rs | 14 +++++++++++--- src/commands/init.rs | 8 ++++++-- src/commands/reset.rs | 8 ++++++-- src/commands/send.rs | 10 +++++++--- src/remote.rs | 32 +++++++++++++++++++++++++++++++- 6 files changed, 72 insertions(+), 13 deletions(-) diff --git a/src/commands/enhance.rs b/src/commands/enhance.rs index f97c6ac..6fac516 100644 --- a/src/commands/enhance.rs +++ b/src/commands/enhance.rs @@ -21,7 +21,7 @@ use zcash_protocol::consensus::{BlockHeight, BranchId, Network}; use crate::{ data::{get_db_paths, get_wallet_network}, - remote::Servers, + remote::{tor_client, Servers}, }; // Options accepted for the `enhance` command @@ -89,7 +89,16 @@ impl Command { anyhow!("Chain height must be available to perform transaction enhancement.") })?; - let mut client = self.server.pick(params)?.connect_direct().await?; + // TODO: + // - Create a shared Tor client. + // - Create an isolated `lightwalletd` connection for each transaction. + // - Spread transactions across all available servers. + // - Fetch transactions in parallel, with timing noise. + let mut client = self + .server + .pick(params)? + .connect(|| tor_client(wallet_dir.as_ref())) + .await?; let mut satisfied_requests = BTreeSet::new(); loop { diff --git a/src/commands/import_ufvk.rs b/src/commands/import_ufvk.rs index 946de70..a2a8986 100644 --- a/src/commands/import_ufvk.rs +++ b/src/commands/import_ufvk.rs @@ -10,7 +10,11 @@ use zcash_client_sqlite::WalletDb; use zcash_keys::keys::UnifiedFullViewingKey; use zcash_primitives::consensus; -use crate::{data::get_db_paths, error, remote::Servers}; +use crate::{ + data::get_db_paths, + error, + remote::{tor_client, Servers}, +}; // Options accepted for the `import-ufvk` command #[derive(Debug, Options)] @@ -45,14 +49,18 @@ impl Command { } }?; - let (_, db_data) = get_db_paths(wallet_dir); + let (_, db_data) = get_db_paths(wallet_dir.as_ref()); let mut db_data = WalletDb::for_path(db_data, params)?; // Construct an `AccountBirthday` for the account's birthday. let birthday = { // Fetch the tree state corresponding to the last block prior to the wallet's // birthday height. NOTE: THIS APPROACH LEAKS THE BIRTHDAY TO THE SERVER! - let mut client = self.server.pick(params)?.connect_direct().await?; + let mut client = self + .server + .pick(params)? + .connect(|| tor_client(wallet_dir)) + .await?; let request = service::BlockId { height: (self.birthday - 1).into(), ..Default::default() diff --git a/src/commands/init.rs b/src/commands/init.rs index 0812256..3eda76b 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -15,7 +15,7 @@ use zcash_primitives::consensus::{self, Parameters}; use crate::{ data::{get_db_paths, init_wallet_config, Network}, error, - remote::Servers, + remote::{tor_client, Servers}, }; // Options accepted for the `init` command @@ -50,7 +50,11 @@ impl Command { let params = consensus::Network::from(opts.network); // Get the current chain height (for the wallet's birthday). - let mut client = opts.server.pick(params)?.connect_direct().await?; + let mut client = opts + .server + .pick(params)? + .connect(|| tor_client(wallet_dir.as_ref())) + .await?; let birthday = if let Some(birthday) = opts.birthday { birthday } else { diff --git a/src/commands/reset.rs b/src/commands/reset.rs index 111cab4..c8b4d36 100644 --- a/src/commands/reset.rs +++ b/src/commands/reset.rs @@ -3,7 +3,7 @@ use gumdrop::Options; use crate::{ data::{erase_wallet_state, read_config}, - remote::Servers, + remote::{tor_client, Servers}, }; // Options accepted for the `reset` command @@ -27,7 +27,11 @@ impl Command { let params = keys.network(); // Connect to the client (for re-initializing the wallet). - let client = self.server.pick(params)?.connect_direct().await?; + let client = self + .server + .pick(params)? + .connect(|| tor_client(wallet_dir.as_ref())) + .await?; // Erase the wallet state (excluding key material). erase_wallet_state(wallet_dir.as_ref()).await; diff --git a/src/commands/send.rs b/src/commands/send.rs index c3b724b..4fb9c9d 100644 --- a/src/commands/send.rs +++ b/src/commands/send.rs @@ -26,7 +26,7 @@ use crate::{ commands::propose::{parse_fee_rule, FeeRule}, data::{get_db_paths, read_config}, error, - remote::Servers, + remote::{tor_client, Servers}, MIN_CONFIRMATIONS, }; @@ -62,7 +62,7 @@ impl Command { let keys = read_config(wallet_dir.as_ref())?; let params = keys.network(); - let (_, db_data) = get_db_paths(wallet_dir); + let (_, db_data) = get_db_paths(wallet_dir.as_ref()); let mut db_data = WalletDb::for_path(db_data, params)?; let account_id = *db_data .get_account_ids()? @@ -87,7 +87,11 @@ impl Command { ) .map_err(error::Error::from)?; - let mut client = self.server.pick(params)?.connect_direct().await?; + let mut client = self + .server + .pick(params)? + .connect(|| tor_client(wallet_dir.as_ref())) + .await?; // Create the transaction. println!("Creating transaction..."); diff --git a/src/remote.rs b/src/remote.rs index 5b2d2eb..64b05f1 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, fmt, path::Path}; +use std::{borrow::Cow, fmt, future::Future, path::Path}; use anyhow::anyhow; use tonic::transport::{Channel, ClientTlsConfig}; @@ -152,6 +152,36 @@ impl<'a> Server<'a> { Ok(CompactTxStreamerClient::new(channel.connect().await?)) } + + async fn connect_over_tor( + &self, + tor: &tor::Client, + ) -> Result, anyhow::Error> { + if !self.use_tls() { + return Err(anyhow!( + "Cannot connect to local lightwalletd server over Tor" + )); + } + + info!("Connecting to {} over Tor", self); + let endpoint = self.endpoint().try_into()?; + Ok(tor.connect_to_lightwalletd(endpoint).await?) + } + + /// Connects to the server over Tor, unless it is running on localhost without HTTPS. + pub(crate) async fn connect( + &self, + tor: impl FnOnce() -> F, + ) -> Result, anyhow::Error> + where + F: Future>, + { + if self.use_tls() { + self.connect_over_tor(&tor().await?).await + } else { + self.connect_direct().await + } + } } pub(crate) async fn tor_client>(