diff --git a/Cargo.toml b/Cargo.toml index d35a981..5d87e4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ hex = "0.3" protobuf = "2" rustyline = "5.0.2" byteorder = "1" -rand = "0.5.6" json = "0.12.0" shellwords = "1.0.0" tiny-bip39 = "0.6.2" @@ -32,47 +31,48 @@ ring = "0.14.0" lazy_static = "1.2.0" tower-service = "0.2" tokio-rustls = "0.10.0-alpha.3" +rustls = { version = "0.15.2", features = ["dangerous_configuration"] } webpki = "0.19.1" webpki-roots = "0.16.0" tower-h2 = { git = "https://github.com/tower-rs/tower-h2" } rust-embed = "5.1.0" +rand = "0.7.2" [dependencies.bellman] git = "https://github.com/adityapk00/librustzcash.git" -rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016" +rev = "188537ea025fcb7fbdfc11266f307a084a5451e4" default-features = false features = ["groth16"] [dependencies.pairing] git = "https://github.com/adityapk00/librustzcash.git" -rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016" +rev = "188537ea025fcb7fbdfc11266f307a084a5451e4" [dependencies.zcash_client_backend] git = "https://github.com/adityapk00/librustzcash.git" -rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016" +rev = "188537ea025fcb7fbdfc11266f307a084a5451e4" default-features = false [dependencies.zcash_primitives] git = "https://github.com/adityapk00/librustzcash.git" -rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016" +rev = "188537ea025fcb7fbdfc11266f307a084a5451e4" default-features = false features = ["transparent-inputs"] [dependencies.zcash_proofs] git = "https://github.com/adityapk00/librustzcash.git" -rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016" +rev = "188537ea025fcb7fbdfc11266f307a084a5451e4" default-features = false [dependencies.ff] git = "https://github.com/adityapk00/librustzcash.git" -rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016" +rev = "188537ea025fcb7fbdfc11266f307a084a5451e4" features = ["ff_derive"] [build-dependencies] tower-grpc-build = { git = "https://github.com/tower-rs/tower-grpc", features = ["tower-hyper"] } -[dev-dependencies] -rand_core = "0.5.1" + [profile.release] debug = false \ No newline at end of file diff --git a/src/commands.rs b/src/commands.rs index ed087a9..e7e627f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use json::{object}; use crate::LightClient; @@ -118,7 +119,7 @@ impl Command for InfoCommand { fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { lightclient.do_sync(true); - LightClient::do_info(lightclient.get_server_uri()) + lightclient.do_info() } } @@ -311,6 +312,31 @@ impl Command for TransactionsCommand { } } +struct HeightCommand {} +impl Command for HeightCommand { + fn help(&self) -> String { + let mut h = vec![]; + h.push("Get the latest block height that the wallet is at"); + h.push("Usage:"); + h.push("height"); + h.push(""); + + h.join("\n") + } + + fn short_help(&self) -> String { + "Get the latest block height that the wallet is at".to_string() + } + + fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { + format!("{}", + object! { + "height" => lightclient.last_scanned_height() + }.pretty(2)) + } +} + + struct NewAddressCommand {} impl Command for NewAddressCommand { fn help(&self) -> String { @@ -407,6 +433,7 @@ pub fn get_commands() -> Box>> { map.insert("help".to_string(), Box::new(HelpCommand{})); map.insert("balance".to_string(), Box::new(BalanceCommand{})); map.insert("addresses".to_string(), Box::new(AddressCommand{})); + map.insert("height".to_string(), Box::new(HeightCommand{})); map.insert("export".to_string(), Box::new(ExportCommand{})); map.insert("info".to_string(), Box::new(InfoCommand{})); map.insert("send".to_string(), Box::new(SendCommand{})); diff --git a/src/grpcconnector.rs b/src/grpcconnector.rs index 073cda4..03bef21 100644 --- a/src/grpcconnector.rs +++ b/src/grpcconnector.rs @@ -24,11 +24,28 @@ use crate::grpc_client::{ChainSpec, BlockId, BlockRange, RawTransaction, TransparentAddressBlockFilter, TxFilter, Empty, LightdInfo}; use crate::grpc_client::client::CompactTxStreamer; +mod danger { + use rustls; + use webpki; + + pub struct NoCertificateVerification {} + + impl rustls::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert(&self, + _roots: &rustls::RootCertStore, + _presented_certs: &[rustls::Certificate], + _dns_name: webpki::DNSNameRef<'_>, + _ocsp: &[u8]) -> Result { + Ok(rustls::ServerCertVerified::assertion()) + } + } +} /// A Secure (https) grpc destination. struct Dst { - addr: SocketAddr, - host: String, + addr: SocketAddr, + host: String, + no_cert: bool, } impl tower_service::Service<()> for Dst { @@ -43,15 +60,24 @@ impl tower_service::Service<()> for Dst { fn call(&mut self, _: ()) -> Self::Future { let mut config = ClientConfig::new(); + config.alpn_protocols.push(b"h2".to_vec()); config.root_store.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + + if self.no_cert { + config.dangerous() + .set_certificate_verifier(Arc::new(danger::NoCertificateVerification {})); + } let config = Arc::new(config); let tls_connector = TlsConnector::from(config); let addr_string_local = self.host.clone(); - let domain = webpki::DNSNameRef::try_from_ascii_str(&addr_string_local).unwrap(); + let domain = match webpki::DNSNameRef::try_from_ascii_str(&addr_string_local) { + Ok(d) => d, + Err(_) => webpki::DNSNameRef::try_from_ascii_str("localhost").unwrap() + }; let domain_local = domain.to_owned(); let stream = TcpStream::connect(&self.addr).and_then(move |sock| { @@ -92,7 +118,7 @@ impl tower_service::Service<()> for Dst { macro_rules! make_grpc_client { - ($protocol:expr, $host:expr, $port:expr) => {{ + ($protocol:expr, $host:expr, $port:expr, $nocert:expr) => {{ let uri: http::Uri = format!("{}://{}", $protocol, $host).parse().unwrap(); let addr = format!("{}:{}", $host, $port) @@ -102,11 +128,11 @@ macro_rules! make_grpc_client { .unwrap(); let h2_settings = Default::default(); - let mut make_client = tower_h2::client::Connect::new(Dst {addr, host: $host.to_string()}, h2_settings, DefaultExecutor::current()); + let mut make_client = tower_h2::client::Connect::new(Dst {addr, host: $host.to_string(), no_cert: $nocert}, h2_settings, DefaultExecutor::current()); make_client .make_service(()) - .map_err(|e| { format!("HTTP/2 connection failed; err={:?}", e) }) + .map_err(|e| { format!("HTTP/2 connection failed; err={:?}.\nIf you're connecting to a local server, please pass --dangerous to trust the server without checking its TLS certificate", e) }) .and_then(move |conn| { let conn = tower_request_modifier::Builder::new() .set_origin(uri) @@ -126,8 +152,8 @@ macro_rules! make_grpc_client { // GRPC code // ============== -pub fn get_info(uri: http::Uri) -> Result { - let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap()) +pub fn get_info(uri: http::Uri, no_cert: bool) -> Result { + let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert) .and_then(move |mut client| { client.get_lightd_info(Request::new(Empty{})) .map_err(|e| { @@ -145,9 +171,9 @@ pub fn get_info(uri: http::Uri) -> Result { } -pub fn fetch_blocks(uri: &http::Uri, start_height: u64, end_height: u64, c: F) - where F : Fn(&[u8]) { - let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap()) +pub fn fetch_blocks(uri: &http::Uri, start_height: u64, end_height: u64, no_cert: bool, mut c: F) + where F : FnMut(&[u8], u64) { + let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert) .and_then(move |mut client| { let bs = BlockId{ height: start_height, hash: vec!()}; let be = BlockId{ height: end_height, hash: vec!()}; @@ -165,7 +191,7 @@ pub fn fetch_blocks(uri: &http::Uri, start_heig let mut encoded_buf = vec![]; b.encode(&mut encoded_buf).unwrap(); - c(&encoded_buf); + c(&encoded_buf, b.height); Ok(()) }) @@ -183,9 +209,9 @@ pub fn fetch_blocks(uri: &http::Uri, start_heig } pub fn fetch_transparent_txids(uri: &http::Uri, address: String, - start_height: u64, end_height: u64,c: F) + start_height: u64, end_height: u64, no_cert: bool, c: F) where F : Fn(&[u8], u64) { - let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap()) + let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert) .and_then(move |mut client| { let start = Some(BlockId{ height: start_height, hash: vec!()}); let end = Some(BlockId{ height: end_height, hash: vec!()}); @@ -218,9 +244,9 @@ pub fn fetch_transparent_txids(uri: &http::Uri, }; } -pub fn fetch_full_tx(uri: &http::Uri, txid: TxId, c: F) +pub fn fetch_full_tx(uri: &http::Uri, txid: TxId, no_cert: bool, c: F) where F : Fn(&[u8]) { - let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap()) + let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert) .and_then(move |mut client| { let txfilter = TxFilter { block: None, index: 0, hash: txid.0.to_vec() }; client.get_transaction(Request::new(txfilter)) @@ -244,8 +270,8 @@ pub fn fetch_full_tx(uri: &http::Uri, txid: TxI }; } -pub fn broadcast_raw_tx(uri: &http::Uri, tx_bytes: Box<[u8]>) -> Result { - let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap()) +pub fn broadcast_raw_tx(uri: &http::Uri, no_cert: bool, tx_bytes: Box<[u8]>) -> Result { + let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert) .and_then(move |mut client| { client.send_transaction(Request::new(RawTransaction {data: tx_bytes.to_vec(), height: 0})) .map_err(|e| { @@ -265,9 +291,9 @@ pub fn broadcast_raw_tx(uri: &http::Uri, tx_bytes: Box<[u8]>) -> Result(uri: &http::Uri, mut c : F) +pub fn fetch_latest_block(uri: &http::Uri, no_cert: bool, mut c : F) where F : FnMut(BlockId) { - let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap()) + let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert) .and_then(|mut client| { client.get_latest_block(Request::new(ChainSpec {})) .map_err(|e| { format!("ERR = {:?}", e) }) diff --git a/src/lightclient.rs b/src/lightclient.rs index 0aa82c2..7e8793b 100644 --- a/src/lightclient.rs +++ b/src/lightclient.rs @@ -1,8 +1,9 @@ use crate::lightwallet::LightWallet; use log::{info, warn, error}; +use rand::{rngs::OsRng, seq::SliceRandom}; -use std::sync::{Arc}; +use std::sync::{Arc, RwLock}; use std::sync::atomic::{AtomicU64, AtomicI32, AtomicUsize, Ordering}; use std::path::Path; use std::fs::File; @@ -33,6 +34,7 @@ pub struct LightClientConfig { pub sapling_activation_height : u64, pub consensus_branch_id : String, pub anchor_offset : u32, + pub no_cert_verification : bool, } impl LightClientConfig { @@ -323,10 +325,20 @@ impl LightClient { self.config.server.clone() } - pub fn do_info(uri: http::Uri) -> String { - let r = get_info(uri); - match r { - Ok(i) => format!("{:?}", i)[11..].to_string(), + pub fn do_info(&self) -> String { + match get_info(self.get_server_uri(), self.config.no_cert_verification) { + Ok(i) => { + let o = object!{ + "version" => i.version, + "vendor" => i.vendor, + "taddr_support" => i.taddr_support, + "chain_name" => i.chain_name, + "sapling_activation_height" => i.sapling_activation_height, + "consensus_branch_id" => i.consensus_branch_id, + "latest_block_height" => i.block_height + }; + o.pretty(2) + }, Err(e) => e } } @@ -562,11 +574,17 @@ impl LightClient { // This will hold the latest block fetched from the RPC let latest_block_height = Arc::new(AtomicU64::new(0)); let lbh = latest_block_height.clone(); - fetch_latest_block(&self.get_server_uri(), move |block: BlockId| { + fetch_latest_block(&self.get_server_uri(), self.config.no_cert_verification, move |block: BlockId| { lbh.store(block.height, Ordering::SeqCst); }); let latest_block = latest_block_height.load(Ordering::SeqCst); + if latest_block < last_scanned_height { + let w = format!("Server's latest block({}) is behind ours({})", latest_block, last_scanned_height); + warn!("{}", w); + return w; + } + info!("Latest block is {}", latest_block); // Get the end height to scan to. @@ -583,6 +601,11 @@ impl LightClient { let mut total_reorg = 0; + // Collect all txns in blocks that we have a tx in. We'll fetch all these + // txs along with our own, so that the server doesn't learn which ones + // belong to us. + let all_new_txs = Arc::new(RwLock::new(vec![])); + // Fetch CompactBlocks in increments loop { let local_light_wallet = self.wallet.clone(); @@ -599,23 +622,26 @@ impl LightClient { // Fetch compact blocks info!("Fetching blocks {}-{}", start_height, end_height); - + let all_txs = all_new_txs.clone(); + let last_invalid_height = Arc::new(AtomicI32::new(0)); let last_invalid_height_inner = last_invalid_height.clone(); - fetch_blocks(&self.get_server_uri(), start_height, end_height, - move |encoded_block: &[u8]| { + fetch_blocks(&self.get_server_uri(), start_height, end_height, self.config.no_cert_verification, + move |encoded_block: &[u8], height: u64| { // Process the block only if there were no previous errors if last_invalid_height_inner.load(Ordering::SeqCst) > 0 { return; } match local_light_wallet.scan_block(encoded_block) { - Ok(_) => {}, + Ok(block_txns) => { + all_txs.write().unwrap().extend_from_slice(&block_txns.iter().map(|txid| (txid.clone(), height as i32)).collect::>()[..]); + }, Err(invalid_height) => { // Block at this height seems to be invalid, so invalidate up till that point last_invalid_height_inner.store(invalid_height, Ordering::SeqCst); } - } + }; local_bytes_downloaded.fetch_add(encoded_block.len(), Ordering::SeqCst); }); @@ -652,7 +678,7 @@ impl LightClient { // TODO: Use for all t addresses let address = self.wallet.address_from_sk(&self.wallet.tkeys.read().unwrap()[0]); let wallet = self.wallet.clone(); - fetch_transparent_txids(&self.get_server_uri(), address, start_height, end_height, + fetch_transparent_txids(&self.get_server_uri(), address, start_height, end_height, self.config.no_cert_verification, move |tx_bytes: &[u8], height: u64 | { let tx = Transaction::read(tx_bytes).unwrap(); @@ -683,21 +709,27 @@ impl LightClient { // We need to first copy over the Txids from the wallet struct, because // we need to free the read lock from here (Because we'll self.wallet.txs later) - let txids_to_fetch: Vec<(TxId, i32)> = self.wallet.txs.read().unwrap().values() + let mut txids_to_fetch: Vec<(TxId, i32)> = self.wallet.txs.read().unwrap().values() .filter(|wtx| wtx.full_tx_scanned == false) .map(|wtx| (wtx.txid, wtx.block)) .collect::>(); - info!("Fetching {} new txids", txids_to_fetch.len()); + info!("Fetching {} new txids, total {} with decoy", txids_to_fetch.len(), all_new_txs.read().unwrap().len()); + txids_to_fetch.extend_from_slice(&all_new_txs.read().unwrap()[..]); + txids_to_fetch.sort(); + txids_to_fetch.dedup(); + + let mut rng = OsRng; + txids_to_fetch.shuffle(&mut rng); // And go and fetch the txids, getting the full transaction, so we can - // read the memos + // read the memos + for (txid, height) in txids_to_fetch { let light_wallet_clone = self.wallet.clone(); info!("Fetching full Tx: {}", txid); - responses.push(format!("Fetching full Tx: {}", txid)); - fetch_full_tx(&self.get_server_uri(), txid, move |tx_bytes: &[u8] | { + fetch_full_tx(&self.get_server_uri(), txid, self.config.no_cert_verification, move |tx_bytes: &[u8] | { let tx = Transaction::read(tx_bytes).unwrap(); light_wallet_clone.scan_full_tx(&tx, height); @@ -716,7 +748,7 @@ impl LightClient { ); match rawtx { - Ok(txbytes) => match broadcast_raw_tx(&self.get_server_uri(), txbytes) { + Ok(txbytes) => match broadcast_raw_tx(&self.get_server_uri(), self.config.no_cert_verification, txbytes) { Ok(k) => k, Err(e) => e, }, diff --git a/src/lightwallet.rs b/src/lightwallet.rs index e674949..f9fe0bb 100644 --- a/src/lightwallet.rs +++ b/src/lightwallet.rs @@ -5,6 +5,8 @@ use std::collections::{HashMap, HashSet}; use std::sync::{Arc, RwLock}; use std::io::{Error, ErrorKind}; +use rand::{Rng, rngs::OsRng}; + use log::{info, warn, error}; use protobuf::parse_from_bytes; @@ -86,26 +88,6 @@ impl ToBase58Check for [u8] { payload.to_base58() } } -// -//pub trait FromBase58Check { -// fn from_base58check(&self, version: &[u8], suffix: &[u8]) -> Vec; -//} -// -// -//impl FromBase58Check for str { -// fn from_base58check(&self, version: &[u8], suffix: &[u8]) -> Vec { -// let mut payload: Vec = Vec::new(); -// let bytes = self.from_base58().unwrap(); -// -// let start = version.len(); -// let end = bytes.len() - (4 + suffix.len()); -// -// payload.extend(&bytes[start..end]); -// -// payload -// } -//} - pub struct LightWallet { seed: [u8; 32], // Seed phrase for this wallet. @@ -165,14 +147,12 @@ impl LightWallet { } pub fn new(seed_phrase: Option, config: &LightClientConfig, latest_block: u64) -> io::Result { - use rand::{FromEntropy, ChaChaRng, Rng}; - // This is the source entropy that corresponds to the 24-word seed phrase let mut seed_bytes = [0u8; 32]; if seed_phrase.is_none() { // Create a random seed. - let mut system_rng = ChaChaRng::from_entropy(); + let mut system_rng = OsRng; system_rng.fill(&mut seed_bytes); } else { seed_bytes.copy_from_slice(&Mnemonic::from_phrase(seed_phrase.expect("should have a seed phrase"), @@ -269,6 +249,9 @@ impl LightWallet { // Write the seed writer.write_all(&self.seed)?; + // Flush after writing the seed, so in case of a disaster, we can still recover the seed. + writer.flush()?; + // Write all the spending keys Vector::write(&mut writer, &self.extsks.read().unwrap(), |w, sk| sk.write(w) @@ -816,8 +799,10 @@ impl LightWallet { // Mark this Tx as scanned { let mut txs = self.txs.write().unwrap(); - let mut wtx = txs.get_mut(&tx.txid()).unwrap(); - wtx.full_tx_scanned = true; + match txs.get_mut(&tx.txid()) { + Some(wtx) => wtx.full_tx_scanned = true, + None => {}, + }; } } @@ -881,8 +866,8 @@ impl LightWallet { } // Scan a block. Will return an error with the block height that failed to scan - pub fn scan_block(&self, block: &[u8]) -> Result<(), i32> { - let block: CompactBlock = match parse_from_bytes(block) { + pub fn scan_block(&self, block_bytes: &[u8]) -> Result, i32> { + let block: CompactBlock = match parse_from_bytes(block_bytes) { Ok(block) => block, Err(e) => { error!("Could not parse CompactBlock from bytes: {}", e); @@ -900,7 +885,7 @@ impl LightWallet { return Err(height); } } - return Ok(()) + return Ok(vec![]); } else if height != (self.last_scanned_height() + 1) { error!( "Block is not height-sequential (expected {}, found {})", @@ -978,7 +963,7 @@ impl LightWallet { .collect(); scan_block( - block, + block.clone(), &self.extfvks.read().unwrap(), &nf_refs[..], &mut block_data.tree, @@ -986,6 +971,18 @@ impl LightWallet { ) }; + // If this block had any new Txs, return the list of ALL txids in this block, + // so the wallet can fetch them all as a decoy. + let all_txs = if !new_txs.is_empty() { + block.vtx.iter().map(|vtx| { + let mut t = [0u8; 32]; + t.copy_from_slice(&vtx.hash[..]); + TxId{0: t} + }).collect::>() + } else { + vec![] + }; + for tx in new_txs { // Mark notes as spent. let mut total_shielded_value_spent: u64 = 0; @@ -1023,9 +1020,7 @@ impl LightWallet { tx_entry.total_shielded_value_spent = total_shielded_value_spent; // Save notes. - for output in tx - .shielded_outputs - .into_iter() + for output in tx.shielded_outputs { info!("Received sapling output"); @@ -1059,7 +1054,8 @@ impl LightWallet { } } - Ok(()) + + Ok(all_txs) } pub fn send_to_address( @@ -1284,9 +1280,10 @@ impl LightWallet { pub mod tests { use std::convert::TryInto; use std::io::{Error}; + use rand::{RngCore, rngs::OsRng}; + use ff::{Field, PrimeField, PrimeFieldRepr}; use pairing::bls12_381::Bls12; - use rand_core::{RngCore, OsRng}; use protobuf::{Message, UnknownFields, CachedSize, RepeatedField}; use zcash_client_backend::{encoding::encode_payment_address, proto::compact_formats::{ @@ -1938,7 +1935,8 @@ pub mod tests { chain_name: "test".to_string(), sapling_activation_height: 0, consensus_branch_id: "000000".to_string(), - anchor_offset: 0 + anchor_offset: 0, + no_cert_verification: false, } } @@ -2860,7 +2858,8 @@ pub mod tests { chain_name: "main".to_string(), sapling_activation_height: 0, consensus_branch_id: "000000".to_string(), - anchor_offset: 1 + anchor_offset: 1, + no_cert_verification: false, }; let seed_phrase = Some("chimney better bulb horror rebuild whisper improve intact letter giraffe brave rib appear bulk aim burst snap salt hill sad merge tennis phrase raise".to_string()); diff --git a/src/main.rs b/src/main.rs index 0e96dbb..c8483f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,11 +8,12 @@ mod commands; use std::io::{Result, Error, ErrorKind}; use std::sync::{Arc}; +use std::sync::mpsc::{channel, Sender, Receiver}; use std::time::Duration; use lightclient::{LightClient, LightClientConfig}; -use log::{info, LevelFilter}; +use log::{info, error, LevelFilter}; use log4rs::append::rolling_file::RollingFileAppender; use log4rs::encode::pattern::PatternEncoder; use log4rs::config::{Appender, Config, Root}; @@ -85,6 +86,14 @@ pub fn main() { .help("Lightwalletd server to connect to.") .takes_value(true) .default_value(lightclient::DEFAULT_SERVER)) + .arg(Arg::with_name("dangerous") + .long("dangerous") + .help("Disable server TLS certificate verification. Use this if you're running a local lightwalletd with a self-signed certificate. WARNING: This is dangerous, don't use it with a server that is not your own.") + .takes_value(false)) + .arg(Arg::with_name("recover") + .long("recover") + .help("Attempt to recover the seed from the wallet") + .takes_value(false)) .arg(Arg::with_name("nosync") .help("By default, zecwallet-cli will sync the wallet at startup. Pass --nosync to prevent the automatic sync at startup.") .long("nosync") @@ -100,6 +109,11 @@ pub fn main() { .multiple(true)) .get_matches(); + if matches.is_present("recover") { + attempt_recover_seed(); + return; + } + let command = matches.value_of("COMMAND"); let params = matches.values_of("PARAMS").map(|v| v.collect()).or(Some(vec![])).unwrap(); @@ -114,68 +128,175 @@ pub fn main() { return; } - // Do a getinfo first, before opening the wallet - let info = match grpcconnector::get_info(server.clone()) { - Ok(ld) => ld, - Err(e) => { - eprintln!("Error:\n{}\nCouldn't get server info, quitting!", e); - return; - } - }; + let dangerous = matches.is_present("dangerous"); + let nosync = matches.is_present("nosync"); - // Create a Light Client Config - let config = lightclient::LightClientConfig { - server : server.clone(), - chain_name : info.chain_name, - sapling_activation_height : info.sapling_activation_height, - consensus_branch_id : info.consensus_branch_id, - anchor_offset : ANCHOR_OFFSET, - }; - - // Configure logging first. - let log_config = match get_log_config(&config) { + let (command_tx, resp_rx) = match startup(server, dangerous, seed, !nosync, command.is_none()) { Ok(c) => c, Err(e) => { - eprintln!("Error:\n{}\nCouldn't configure logging, quitting!", e); + eprintln!("Error during startup: {}", e); + error!("Error during startup: {}", e); return; } }; - log4rs::init_config(log_config).unwrap(); - // Startup + if command.is_none() { + start_interactive(command_tx, resp_rx); + } else { + command_tx.send( + (command.unwrap().to_string(), + params.iter().map(|s| s.to_string()).collect::>())) + .unwrap(); + + match resp_rx.recv() { + Ok(s) => println!("{}", s), + Err(e) => { + let e = format!("Error executing command {}: {}", command.unwrap(), e); + eprintln!("{}", e); + error!("{}", e); + } + } + } +} + +fn startup(server: http::Uri, dangerous: bool, seed: Option, first_sync: bool, print_updates: bool) + -> Result<(Sender<(String, Vec)>, Receiver)> { + // Try to get the configuration + let (config, latest_block_height) = create_lightclient_config(server.clone(), dangerous)?; + + // Configure logging first. + let log_config = get_log_config(&config)?; + log4rs::init_config(log_config).map_err(|e| { + std::io::Error::new(ErrorKind::Other, e) + })?; + + let lightclient = Arc::new(create_lightclient(seed, latest_block_height, &config)?); + + // Print startup Messages info!(""); // Blank line info!("Starting Zecwallet-CLI"); info!("Light Client config {:?}", config); - let lightclient = match LightClient::new(seed, &config, info.block_height) { - Ok(lc) => Arc::new(lc), - Err(e) => { eprintln!("Failed to start wallet. Error was:\n{}", e); return; } - }; + if print_updates { + println!("Lightclient connecting to {}", config.server); + } - // At startup, run a sync. - let sync_output = if matches.is_present("nosync") { - None - } else { - Some(lightclient.do_sync(true)) - }; + // Start the command loop + let (command_tx, resp_rx) = command_loop(lightclient.clone()); - if command.is_none() { - // If running in interactive mode, output of the sync command - if sync_output.is_some() { - println!("{}", sync_output.unwrap()); + // At startup, run a sync. + if first_sync { + let update = lightclient.do_sync(true); + if print_updates { + println!("{}", update); + } + } + + Ok((command_tx, resp_rx)) +} + +fn create_lightclient_config(server: http::Uri, dangerous: bool) -> Result<(LightClientConfig, u64)> { + // Do a getinfo first, before opening the wallet + let info = grpcconnector::get_info(server.clone(), dangerous) + .map_err(|e| std::io::Error::new(ErrorKind::ConnectionRefused, e))?; + + // Create a Light Client Config + let config = lightclient::LightClientConfig { + server, + chain_name : info.chain_name, + sapling_activation_height : info.sapling_activation_height, + consensus_branch_id : info.consensus_branch_id, + anchor_offset : ANCHOR_OFFSET, + no_cert_verification : dangerous, + }; + + Ok((config, info.block_height)) +} + +fn create_lightclient(seed: Option, latest_block: u64, config: &LightClientConfig) -> Result<(LightClient)> { + let lightclient = LightClient::new(seed, config, latest_block)?; + + Ok(lightclient) +} + +fn start_interactive(command_tx: Sender<(String, Vec)>, resp_rx: Receiver) { + // `()` can be used when no completer is required + let mut rl = Editor::<()>::new(); + + println!("Ready!"); + + let send_command = |cmd: String, args: Vec| -> String { + command_tx.send((cmd.clone(), args)).unwrap(); + match resp_rx.recv() { + Ok(s) => s, + Err(e) => { + let e = format!("Error executing command {}: {}", cmd, e); + eprintln!("{}", e); + error!("{}", e); + return "".to_string() + } + } + }; + + let info = &send_command("info".to_string(), vec![]); + let chain_name = json::parse(info).unwrap()["chain_name"].as_str().unwrap().to_string(); + + loop { + // Read the height first + let height = json::parse(&send_command("height".to_string(), vec![])).unwrap()["height"].as_i64().unwrap(); + + let readline = rl.readline(&format!("({}) Block:{} (type 'help') >> ", + chain_name, height)); + match readline { + Ok(line) => { + rl.add_history_entry(line.as_str()); + // Parse command line arguments + let mut cmd_args = match shellwords::split(&line) { + Ok(args) => args, + Err(_) => { + println!("Mismatched Quotes"); + continue; + } + }; + + if cmd_args.is_empty() { + continue; + } + + let cmd = cmd_args.remove(0); + let args: Vec = cmd_args; + + println!("{}", send_command(cmd, args)); + + // Special check for Quit command. + if line == "quit" { + break; + } + }, + Err(ReadlineError::Interrupted) => { + println!("CTRL-C"); + info!("CTRL-C"); + println!("{}", send_command("save".to_string(), vec![])); + break + }, + Err(ReadlineError::Eof) => { + println!("CTRL-D"); + info!("CTRL-D"); + println!("{}", send_command("save".to_string(), vec![])); + break + }, + Err(err) => { + println!("Error: {:?}", err); + break + } } - start_interactive(lightclient, &config); - } else { - let cmd_response = commands::do_user_command(&command.unwrap(), ¶ms, lightclient.as_ref()); - println!("{}", cmd_response); } } -fn start_interactive(lightclient: Arc, config: &LightClientConfig) { - println!("Lightclient connecting to {}", config.server); - let (command_tx, command_rx) = std::sync::mpsc::channel::<(String, Vec)>(); - let (resp_tx, resp_rx) = std::sync::mpsc::channel::(); +fn command_loop(lightclient: Arc) -> (Sender<(String, Vec)>, Receiver) { + let (command_tx, command_rx) = channel::<(String, Vec)>(); + let (resp_tx, resp_rx) = channel::(); let lc = lightclient.clone(); std::thread::spawn(move || { @@ -201,63 +322,35 @@ fn start_interactive(lightclient: Arc, config: &LightClientConfig) } }); - // `()` can be used when no completer is required - let mut rl = Editor::<()>::new(); + (command_tx, resp_rx) +} - println!("Ready!"); +fn attempt_recover_seed() { + use std::fs::File; + use std::io::prelude::*; + use std::io::{BufReader}; + use byteorder::{LittleEndian, ReadBytesExt,}; + use bip39::{Mnemonic, Language}; - loop { - let readline = rl.readline(&format!("({}) Block:{} (type 'help') >> ", - config.chain_name, - lightclient.last_scanned_height())); - match readline { - Ok(line) => { - rl.add_history_entry(line.as_str()); - // Parse command line arguments - let mut cmd_args = match shellwords::split(&line) { - Ok(args) => args, - Err(_) => { - println!("Mismatched Quotes"); - continue; - } - }; + // Create a Light Client Config in an attempt to recover the file. + let config = LightClientConfig { + server: "0.0.0.0:0".parse().unwrap(), + chain_name: "main".to_string(), + sapling_activation_height: 0, + consensus_branch_id: "000000".to_string(), + anchor_offset: 0, + no_cert_verification: false, + }; - if cmd_args.is_empty() { - continue; - } + let mut reader = BufReader::new(File::open(config.get_wallet_path()).unwrap()); + let version = reader.read_u64::().unwrap(); + println!("Reading wallet version {}", version); - let cmd = cmd_args.remove(0); - let args: Vec = cmd_args; - command_tx.send((cmd, args)).unwrap(); + // Seed + let mut seed_bytes = [0u8; 32]; + reader.read_exact(&mut seed_bytes).unwrap(); - // Wait for the response - match resp_rx.recv() { - Ok(response) => println!("{}", response), - _ => { eprintln!("Error receiving response");} - } - - // Special check for Quit command. - if line == "quit" { - break; - } - }, - Err(ReadlineError::Interrupted) => { - println!("CTRL-C"); - info!("CTRL-C"); - println!("{}", lightclient.do_save()); - break - }, - Err(ReadlineError::Eof) => { - println!("CTRL-D"); - info!("CTRL-D"); - println!("{}", lightclient.do_save()); - break - }, - Err(err) => { - println!("Error: {:?}", err); - break - } - } - } + let phrase = Mnemonic::from_entropy(&seed_bytes, Language::English,).unwrap().phrase().to_string(); + println!("Recovered seed phrase:\n{}", phrase); } \ No newline at end of file