From bd15a081b19e788674a34eec4229cb0f8114eaea Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 18 Oct 2019 10:32:05 -0700 Subject: [PATCH 01/24] Allow multiple addresses in send --- lib/src/commands.rs | 65 ++++++++++++++++++++++++++++++++++-------- lib/src/lightclient.rs | 7 +++-- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/lib/src/commands.rs b/lib/src/commands.rs index c4cb356..7f4b2de 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -209,6 +209,8 @@ impl Command for SendCommand { h.push("Send ZEC to a given address"); h.push("Usage:"); h.push("send
\"optional_memo\""); + h.push("OR"); + h.push("send '[{'address':
, 'amount': , 'memo': }, ...]'"); h.push(""); h.push("Example:"); h.push("send ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d 200000 \"Hello from the command line\""); @@ -222,25 +224,64 @@ impl Command for SendCommand { } fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { - // Parse the args. + // Parse the args. There are two argument types. + // 1 - A set of 2(+1 optional) arguments for a single address send representing address, value, memo? + // 2 - A single argument in the form of a JSON string that is "[{address: address, value: value, memo: memo},...]" + // 1 - Destination address. T or Z address - if args.len() < 2 || args.len() > 3 { + if args.len() < 1 || args.len() > 3 { return self.help(); } - // Make sure we can parse the amount - let value = match args[1].parse::() { - Ok(amt) => amt, - Err(e) => { - return format!("Couldn't parse amount: {}", e); + // Check for a single argument that can be parsed as JSON + if args.len() == 1 { + // Sometimes on the command line, people use "'" for the quotes, which json::parse doesn't + // understand. So replace it with double-quotes + let arg_list = args[0].replace("'", "\""); + + let json_args = match json::parse(&arg_list) { + Ok(j) => j, + Err(e) => { + let es = format!("Couldn't understand JSON: {}", e); + return format!("{}\n{}", es, self.help()); + } + }; + + if !json_args.is_array() { + return format!("Couldn't parse argument as array\n{}", self.help()); } - }; - let memo = if args.len() == 3 { Some(args[2].to_string()) } else {None}; - - lightclient.do_sync(true); + let maybe_send_args = json_args.members().map( |j| { + if !j.has_key("address") || !j.has_key("amount") { + Err(format!("Need 'address' and 'amount'\n")) + } else { + Ok((j["address"].as_str().unwrap(), j["amount"].as_u64().unwrap(), j["memo"].as_str().map(|s| s.to_string()))) + } + }).collect::)>, String>>(); + + let send_args = match maybe_send_args { + Ok(a) => a, + Err(s) => { return format!("Error: {}\n{}", s, self.help()); } + }; + + lightclient.do_sync(true); + return lightclient.do_send(send_args); + } else if args.len() == 2 || args.len() == 3 { + // Make sure we can parse the amount + let value = match args[1].parse::() { + Ok(amt) => amt, + Err(e) => { + return format!("Couldn't parse amount: {}", e); + } + }; + + let memo = if args.len() == 3 { Some(args[2].to_string()) } else {None}; + + lightclient.do_sync(true); + return lightclient.do_send(vec!((args[0], value, memo))); + } - lightclient.do_send(args[0], value, memo) + self.help() } } diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 56b62f8..4850ad5 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -757,12 +757,13 @@ impl LightClient { responses.join("\n") } - pub fn do_send(&self, addr: &str, value: u64, memo: Option) -> String { + pub fn do_send(&self, addrs: Vec<(&str, u64, Option)>) -> String { info!("Creating transaction"); + let rawtx = self.wallet.send_to_address( - u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), // Blossom ID + u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), &self.sapling_spend, &self.sapling_output, - vec![(&addr, value, memo)] + addrs ); match rawtx { From 4518965a41f7e4b253a4943a760af76fefc878df Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 18 Oct 2019 12:38:28 -0700 Subject: [PATCH 02/24] Return txid from send --- lib/src/grpcconnector.rs | 14 ++++++++++++-- lib/src/lightclient.rs | 2 +- lib/src/lightwallet/data.rs | 1 + 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/src/grpcconnector.rs b/lib/src/grpcconnector.rs index 03bef21..0f22c67 100644 --- a/lib/src/grpcconnector.rs +++ b/lib/src/grpcconnector.rs @@ -1,10 +1,11 @@ - use log::{error}; use std::sync::{Arc}; use std::net::ToSocketAddrs; use std::net::SocketAddr; +use json::object; + use futures::{Future}; use futures::stream::Stream; @@ -280,7 +281,16 @@ pub fn broadcast_raw_tx(uri: &http::Uri, no_cert: bool, tx_bytes: Box<[u8]>) -> .and_then(move |response| { let sendresponse = response.into_inner(); if sendresponse.error_code == 0 { - Ok(format!("Successfully broadcast Tx: {}", sendresponse.error_message)) + let mut txid = sendresponse.error_message; + if txid.starts_with("\"") && txid.ends_with("\"") { + txid = txid[1..txid.len()-1].to_string(); + } + + let r = object!{ + "result" => "success", + "txid" => txid, + }; + Ok(r.pretty(2)) } else { Err(format!("Error: {:?}", sendresponse)) } diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 4850ad5..2afb5db 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -771,7 +771,7 @@ impl LightClient { Ok(k) => k, Err(e) => e, }, - Err(e) => format!("No Tx to broadcast. Error was: {}", e) + Err(e) => format!("Error: No Tx to broadcast. Error was: {}", e) } } } diff --git a/lib/src/lightwallet/data.rs b/lib/src/lightwallet/data.rs index 8ee5c80..5a2a586 100644 --- a/lib/src/lightwallet/data.rs +++ b/lib/src/lightwallet/data.rs @@ -386,6 +386,7 @@ pub struct WalletTx { // All outgoing sapling sends to addresses outside this wallet pub outgoing_metadata: Vec, + // Whether this TxID was downloaded from the server and scanned for Memos pub full_tx_scanned: bool, } From 5905ef728b2813a646e21870ae91c4f568e87bb1 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 18 Oct 2019 15:38:16 -0700 Subject: [PATCH 03/24] Save in non-interactive mode --- lib/src/lightclient.rs | 18 ++++++++++++++---- src/main.rs | 4 ++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 2afb5db..e527c2d 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -333,10 +333,20 @@ impl LightClient { 1_000_000, // 1 MB write buffer File::create(self.config.get_wallet_path()).unwrap()); - self.wallet.write(&mut file_buffer).unwrap(); - info!("Saved wallet"); - - format!("Saved Wallet") + match self.wallet.write(&mut file_buffer) { + Ok(_) => { + info!("Saved wallet"); + let response = object!{ + "result" => "success" + }; + response.pretty(2) + }, + Err(e) => { + let err = format!("ERR: {}", e); + error!("{}", err); + err + } + } } pub fn get_server_uri(&self) -> http::Uri { diff --git a/src/main.rs b/src/main.rs index 6881f8b..96fde21 100644 --- a/src/main.rs +++ b/src/main.rs @@ -137,6 +137,10 @@ pub fn main() { error!("{}", e); } } + + // Save before exit + command_tx.send(("save".to_string(), vec![])).unwrap(); + resp_rx.recv().unwrap(); } } From ebf3c71339849a87b56b48fe80243c40674e1022 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 18 Oct 2019 16:54:26 -0700 Subject: [PATCH 04/24] Add datetime to transactions --- lib/src/lightclient.rs | 119 ++++++++++++++++++------------------ lib/src/lightwallet.rs | 52 ++++++++-------- lib/src/lightwallet/data.rs | 18 +++++- 3 files changed, 103 insertions(+), 86 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index e527c2d..b19f2f6 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -7,10 +7,13 @@ use std::sync::{Arc, RwLock}; use std::sync::atomic::{AtomicU64, AtomicI32, AtomicUsize, Ordering}; use std::path::Path; use std::fs::File; +use std::collections::HashMap; use std::io; use std::io::prelude::*; use std::io::{BufReader, BufWriter, Error, ErrorKind}; +use protobuf::parse_from_bytes; + use json::{object, array, JsonValue}; use zcash_primitives::transaction::{TxId, Transaction}; use zcash_client_backend::{ @@ -393,6 +396,7 @@ impl LightClient { } else { Some(object!{ "created_in_block" => wtx.block, + "datetime" => wtx.datetime, "created_in_txid" => format!("{}", txid), "value" => nd.note.value, "is_change" => nd.is_change, @@ -413,71 +417,50 @@ impl LightClient { } }); - // Collect UTXOs - let utxos = self.wallet.get_utxos().iter() - .filter(|utxo| utxo.unconfirmed_spent.is_none()) // Filter out unconfirmed from the list of utxos - .map(|utxo| { - object!{ - "created_in_block" => utxo.height, - "created_in_txid" => format!("{}", utxo.txid), - "value" => utxo.value, - "scriptkey" => hex::encode(utxo.script.clone()), - "is_change" => false, // TODO: Identify notes as change if we send change to taddrs - "address" => utxo.address.clone(), - "spent" => utxo.spent.map(|spent_txid| format!("{}", spent_txid)), - "unconfirmed_spent" => utxo.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), - } - }) - .collect::>(); + let mut unspent_utxos: Vec = vec![]; + let mut spent_utxos : Vec = vec![]; + let mut pending_utxos: Vec = vec![]; - // Collect pending UTXOs - let pending_utxos = self.wallet.get_utxos().iter() - .filter(|utxo| utxo.unconfirmed_spent.is_some()) // Filter to include only unconfirmed utxos - .map(|utxo| - object!{ - "created_in_block" => utxo.height, - "created_in_txid" => format!("{}", utxo.txid), - "value" => utxo.value, - "scriptkey" => hex::encode(utxo.script.clone()), - "is_change" => false, // TODO: Identify notes as change if we send change to taddrs - "address" => utxo.address.clone(), - "spent" => utxo.spent.map(|spent_txid| format!("{}", spent_txid)), - "unconfirmed_spent" => utxo.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), + self.wallet.txs.read().unwrap().iter() + .flat_map( |(txid, wtx)| { + wtx.utxos.iter().filter_map(move |utxo| + if !all_notes && utxo.spent.is_some() { + None + } else { + Some(object!{ + "created_in_block" => wtx.block, + "datetime" => wtx.datetime, + "created_in_txid" => format!("{}", txid), + "value" => utxo.value, + "scriptkey" => hex::encode(utxo.script.clone()), + "is_change" => false, // TODO: Identify notes as change if we send change to taddrs + "address" => utxo.address.clone(), + "spent" => utxo.spent.map(|spent_txid| format!("{}", spent_txid)), + "unconfirmed_spent" => utxo.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), + }) + } + ) + }) + .for_each( |utxo| { + if utxo["spent"].is_null() && utxo["unconfirmed_spent"].is_null() { + unspent_utxos.push(utxo); + } else if !utxo["spent"].is_null() { + spent_utxos.push(utxo); + } else { + pending_utxos.push(utxo); } - ) - .collect::>(); + }); let mut res = object!{ "unspent_notes" => unspent_notes, "pending_notes" => pending_notes, - "utxos" => utxos, + "utxos" => unspent_utxos, "pending_utxos" => pending_utxos, }; if all_notes { res["spent_notes"] = JsonValue::Array(spent_notes); - } - - // If all notes, also add historical utxos - if all_notes { - res["spent_utxos"] = JsonValue::Array(self.wallet.txs.read().unwrap().values() - .flat_map(|wtx| { - wtx.utxos.iter() - .filter(|utxo| utxo.spent.is_some()) - .map(|utxo| { - object!{ - "created_in_block" => wtx.block, - "created_in_txid" => format!("{}", utxo.txid), - "value" => utxo.value, - "scriptkey" => hex::encode(utxo.script.clone()), - "is_change" => false, // TODO: Identify notes as change if we send change to taddrs - "address" => utxo.address.clone(), - "spent" => utxo.spent.map(|spent_txid| format!("{}", spent_txid)), - "unconfirmed_spent" => utxo.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), - } - }).collect::>() - }).collect::>() - ); + res["spent_utxos"] = JsonValue::Array(spent_utxos); } res @@ -511,6 +494,7 @@ impl LightClient { txns.push(object! { "block_height" => v.block, + "datetime" => v.datetime, "txid" => format!("{}", v.txid), "amount" => total_change as i64 - v.total_shielded_value_spent as i64 @@ -525,6 +509,7 @@ impl LightClient { .map ( |nd| object! { "block_height" => v.block, + "datetime" => v.datetime, "txid" => format!("{}", v.txid), "amount" => nd.note.value as i64, "address" => self.wallet.note_address(nd), @@ -538,6 +523,7 @@ impl LightClient { // Create an input transaction for the transparent value as well. txns.push(object!{ "block_height" => v.block, + "datetime" => v.datetime, "txid" => format!("{}", v.txid), "amount" => total_transparent_received as i64 - v.total_transparent_value_spent as i64, "address" => v.utxos.iter().map(|u| u.address.clone()).collect::>().join(","), @@ -636,6 +622,10 @@ impl LightClient { // Fetch CompactBlocks in increments loop { + // Collect all block times, because we'll need to update transparent tx + // datetime via the block height timestamp + let block_times = Arc::new(RwLock::new(HashMap::new())); + let local_light_wallet = self.wallet.clone(); let local_bytes_downloaded = bytes_downloaded.clone(); @@ -650,7 +640,9 @@ impl LightClient { // Fetch compact blocks info!("Fetching blocks {}-{}", start_height, end_height); + let all_txs = all_new_txs.clone(); + let block_times_inner = block_times.clone(); let last_invalid_height = Arc::new(AtomicI32::new(0)); let last_invalid_height_inner = last_invalid_height.clone(); @@ -661,8 +653,18 @@ impl LightClient { return; } + let block: Result + = parse_from_bytes(encoded_block); + match block { + Ok(b) => { + block_times_inner.write().unwrap().insert(b.height, b.time); + }, + Err(_) => {} + } + match local_light_wallet.scan_block(encoded_block) { Ok(block_txns) => { + // Add to global tx list all_txs.write().unwrap().extend_from_slice(&block_txns.iter().map(|txid| (txid.clone(), height as i32)).collect::>()[..]); }, Err(invalid_height) => { @@ -711,7 +713,8 @@ impl LightClient { let tx = Transaction::read(tx_bytes).unwrap(); // Scan this Tx for transparent inputs and outputs - wallet.scan_full_tx(&tx, height as i32); + let datetime = block_times.read().unwrap().get(&height).map(|v| *v).unwrap_or(0); + wallet.scan_full_tx(&tx, height as i32, datetime as u64); } ); @@ -722,8 +725,9 @@ impl LightClient { break; } else if end_height > latest_block { end_height = latest_block; - } + } } + if print_updates{ println!(""); // New line to finish up the updates } @@ -752,7 +756,6 @@ impl LightClient { // And go and fetch the txids, getting the full transaction, so we can // read the memos - for (txid, height) in txids_to_fetch { let light_wallet_clone = self.wallet.clone(); info!("Fetching full Tx: {}", txid); @@ -760,7 +763,7 @@ impl LightClient { 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); + light_wallet_clone.scan_full_tx(&tx, height, 0); }); }; diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 80b0d86..0db9278 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -560,12 +560,12 @@ impl LightWallet { .sum::() } - fn add_toutput_to_wtx(&self, height: i32, txid: &TxId, vout: &TxOut, n: u64) { + fn add_toutput_to_wtx(&self, height: i32, timestamp: u64, txid: &TxId, vout: &TxOut, n: u64) { let mut txs = self.txs.write().unwrap(); // Find the existing transaction entry, or create a new one. if !txs.contains_key(&txid) { - let tx_entry = WalletTx::new(height, &txid); + let tx_entry = WalletTx::new(height, timestamp, &txid); txs.insert(txid.clone(), tx_entry); } let tx_entry = txs.get_mut(&txid).unwrap(); @@ -600,7 +600,7 @@ impl LightWallet { } // Scan the full Tx and update memos for incoming shielded transactions - pub fn scan_full_tx(&self, tx: &Transaction, height: i32) { + pub fn scan_full_tx(&self, tx: &Transaction, height: i32, datetime: u64) { // Scan all the inputs to see if we spent any transparent funds in this tx // TODO: Save this object @@ -639,7 +639,7 @@ impl LightWallet { let mut txs = self.txs.write().unwrap(); if !txs.contains_key(&tx.txid()) { - let tx_entry = WalletTx::new(height, &tx.txid()); + let tx_entry = WalletTx::new(height, datetime, &tx.txid()); txs.insert(tx.txid().clone(), tx_entry); } @@ -661,7 +661,7 @@ impl LightWallet { Some(TransparentAddress::PublicKey(hash)) => { if hash[..] == ripemd160::Ripemd160::digest(&Sha256::digest(&pubkey))[..] { // This is our address. Add this as an output to the txid - self.add_toutput_to_wtx(height, &tx.txid(), &vout, n as u64); + self.add_toutput_to_wtx(height, datetime, &tx.txid(), &vout, n as u64); } }, _ => {} @@ -1013,7 +1013,7 @@ impl LightWallet { // Find the existing transaction entry, or create a new one. if !txs.contains_key(&tx.txid) { - let tx_entry = WalletTx::new(block_data.height as i32, &tx.txid); + let tx_entry = WalletTx::new(block_data.height as i32, block.time as u64, &tx.txid); txs.insert(tx.txid, tx_entry); } let tx_entry = txs.get_mut(&tx.txid).unwrap(); @@ -1669,7 +1669,7 @@ pub mod tests { tx.add_t_output(&pk, AMOUNT1); let txid1 = tx.get_tx().txid(); - wallet.scan_full_tx(&tx.get_tx(), 100); // Pretend it is at height 100 + wallet.scan_full_tx(&tx.get_tx(), 100, 0); // Pretend it is at height 100 { let txs = wallet.txs.read().unwrap(); @@ -1694,7 +1694,7 @@ pub mod tests { tx.add_t_input(txid1, 0); let txid2 = tx.get_tx().txid(); - wallet.scan_full_tx(&tx.get_tx(), 101); // Pretent it is at height 101 + wallet.scan_full_tx(&tx.get_tx(), 101, 0); // Pretent it is at height 101 { // Make sure the txid was spent @@ -1741,7 +1741,7 @@ pub mod tests { tx.add_t_output(&non_wallet_pk, 25); let txid1 = tx.get_tx().txid(); - wallet.scan_full_tx(&tx.get_tx(), 100); // Pretend it is at height 100 + wallet.scan_full_tx(&tx.get_tx(), 100, 0); // Pretend it is at height 100 { let txs = wallet.txs.read().unwrap(); @@ -1766,7 +1766,7 @@ pub mod tests { tx.add_t_input(txid1, 1); // Ours was at position 1 in the input tx let txid2 = tx.get_tx().txid(); - wallet.scan_full_tx(&tx.get_tx(), 101); // Pretent it is at height 101 + wallet.scan_full_tx(&tx.get_tx(), 101, 0); // Pretent it is at height 101 { // Make sure the txid was spent @@ -1815,7 +1815,7 @@ pub mod tests { let mut tx = FakeTransaction::new_with_txid(txid1); tx.add_t_output(&pk, TAMOUNT1); - wallet.scan_full_tx(&tx.get_tx(), 0); // Height 0 + wallet.scan_full_tx(&tx.get_tx(), 0, 0); // Height 0 const AMOUNT2:u64 = 2; @@ -1828,7 +1828,7 @@ pub mod tests { let mut tx = FakeTransaction::new_with_txid(txid2); tx.add_t_input(txid1, 0); - wallet.scan_full_tx(&tx.get_tx(), 1); // Height 1 + wallet.scan_full_tx(&tx.get_tx(), 1, 0); // Height 1 // Now, the original note should be spent and there should be a change assert_eq!(wallet.zbalance(None), AMOUNT1 - AMOUNT2 ); // The t addr amount is received + spent, so it cancels out @@ -2023,7 +2023,7 @@ pub mod tests { } // Now, full scan the Tx, which should populate the Outgoing Meta data - wallet.scan_full_tx(&sent_tx, 2); + wallet.scan_full_tx(&sent_tx, 2, 0); // Check Outgoing Metadata { @@ -2063,7 +2063,7 @@ pub mod tests { let mut cb3 = FakeCompactBlock::new(2, block_hash); cb3.add_tx(&sent_tx); wallet.scan_block(&cb3.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 2); + wallet.scan_full_tx(&sent_tx, 2, 0); // Because the builder will randomize notes outputted, we need to find // which note number is the change and which is the output note (Because this tx @@ -2116,7 +2116,7 @@ pub mod tests { let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); cb4.add_tx(&sent_tx); wallet.scan_block(&cb4.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 3); + wallet.scan_full_tx(&sent_tx, 3, 0); { // Both notes should be spent now. @@ -2187,7 +2187,7 @@ pub mod tests { } // Now, full scan the Tx, which should populate the Outgoing Meta data - wallet.scan_full_tx(&sent_tx, 2); + wallet.scan_full_tx(&sent_tx, 2, 0); // Check Outgoing Metadata for t address { @@ -2215,7 +2215,7 @@ pub mod tests { tx.add_t_output(&pk, AMOUNT_T); let txid_t = tx.get_tx().txid(); - wallet.scan_full_tx(&tx.get_tx(), 1); // Pretend it is at height 1 + wallet.scan_full_tx(&tx.get_tx(), 1, 0); // Pretend it is at height 1 { let txs = wallet.txs.read().unwrap(); @@ -2277,7 +2277,7 @@ pub mod tests { // Scan the compact block and the full Tx wallet.scan_block(&cb3.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 2); + wallet.scan_full_tx(&sent_tx, 2, 0); // Now this new Spent tx should be in, so the note should be marked confirmed spent { @@ -2327,7 +2327,7 @@ pub mod tests { wallet.scan_block(&cb3.as_bytes()).unwrap(); // And scan the Full Tx to get the memo - wallet.scan_full_tx(&sent_tx, 2); + wallet.scan_full_tx(&sent_tx, 2, 0); { let txs = wallet.txs.read().unwrap(); @@ -2366,7 +2366,7 @@ pub mod tests { wallet.scan_block(&cb3.as_bytes()).unwrap(); // And scan the Full Tx to get the memo - wallet.scan_full_tx(&sent_tx, 2); + wallet.scan_full_tx(&sent_tx, 2, 0); { let txs = wallet.txs.read().unwrap(); @@ -2424,7 +2424,7 @@ pub mod tests { let mut cb3 = FakeCompactBlock::new(2, block_hash); cb3.add_tx(&sent_tx); wallet.scan_block(&cb3.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 2); + wallet.scan_full_tx(&sent_tx, 2, 0); // Check that the send to the second taddr worked { @@ -2468,7 +2468,7 @@ pub mod tests { let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); cb4.add_tx(&sent_tx); wallet.scan_block(&cb4.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 3); + wallet.scan_full_tx(&sent_tx, 3, 0); // Quickly check we have it { @@ -2505,7 +2505,7 @@ pub mod tests { let mut cb5 = FakeCompactBlock::new(4, cb4.hash()); cb5.add_tx(&sent_tx); wallet.scan_block(&cb5.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 4); + wallet.scan_full_tx(&sent_tx, 4, 0); { let txs = wallet.txs.read().unwrap(); @@ -2561,7 +2561,7 @@ pub mod tests { let mut cb3 = FakeCompactBlock::new(2, block_hash); cb3.add_tx(&sent_tx); wallet.scan_block(&cb3.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 2); + wallet.scan_full_tx(&sent_tx, 2, 0); // Make sure all the outputs are there! { @@ -2633,7 +2633,7 @@ pub mod tests { let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); cb4.add_tx(&sent_tx); wallet.scan_block(&cb4.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 3); + wallet.scan_full_tx(&sent_tx, 3, 0); // Make sure all the outputs are there! { @@ -2811,7 +2811,7 @@ pub mod tests { let mut cb3 = FakeCompactBlock::new(7, blk6_hash); cb3.add_tx(&sent_tx); wallet.scan_block(&cb3.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 7); + wallet.scan_full_tx(&sent_tx, 7, 0); // Make sure the Tx is in. { diff --git a/lib/src/lightwallet/data.rs b/lib/src/lightwallet/data.rs index 5a2a586..8660f5d 100644 --- a/lib/src/lightwallet/data.rs +++ b/lib/src/lightwallet/data.rs @@ -362,8 +362,12 @@ impl OutgoingTxMetadata { } pub struct WalletTx { + // Block in which this tx was included pub block: i32, + // Timestamp of Tx. Added in v4 + pub datetime: u64, + // Txid of this transaction. It's duplicated here (It is also the Key in the HashMap that points to this // WalletTx in LightWallet::txs) pub txid: TxId, @@ -392,12 +396,13 @@ pub struct WalletTx { impl WalletTx { pub fn serialized_version() -> u64 { - return 3; + return 4; } - pub fn new(height: i32, txid: &TxId) -> Self { + pub fn new(height: i32, datetime: u64, txid: &TxId) -> Self { WalletTx { block: height, + datetime, txid: txid.clone(), notes: vec![], utxos: vec![], @@ -414,6 +419,12 @@ impl WalletTx { let block = reader.read_i32::()?; + let datetime = if version >= 4 { + reader.read_u64::()? + } else { + 0 + }; + let mut txid_bytes = [0u8; 32]; reader.read_exact(&mut txid_bytes)?; @@ -432,6 +443,7 @@ impl WalletTx { Ok(WalletTx{ block, + datetime, txid, notes, utxos, @@ -447,6 +459,8 @@ impl WalletTx { writer.write_i32::(self.block)?; + writer.write_u64::(self.datetime)?; + writer.write_all(&self.txid.0)?; Vector::write(&mut writer, &self.notes, |w, nd| nd.write(w))?; From e3a0fd2dea59c0cf6434148bf77fef84830cce02 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 18 Oct 2019 17:04:44 -0700 Subject: [PATCH 05/24] Support for wallet encryption --- lib/src/lightwallet.rs | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 0db9278..99e1698 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -90,13 +90,15 @@ impl ToBase58Check for [u8] { } pub struct LightWallet { - seed: [u8; 32], // Seed phrase for this wallet. + locked: bool, // Is the wallet's spending keys locked? - // List of keys, actually in this wallet. This may include more - // than keys derived from the seed, for example, if user imports - // a private key + seed: [u8; 32], // Seed phrase for this wallet. If wallet is locked, this is encrypted + + // List of keys, actually in this wallet. If the wallet is locked, the `extsks` will be + // encrypted (but the fvks are not encrpyted) extsks: Arc>>, extfvks: Arc>>, + pub address: Arc>>>, // Transparent keys. TODO: Make it not pubic @@ -115,7 +117,7 @@ pub struct LightWallet { impl LightWallet { pub fn serialized_version() -> u64 { - return 3; + return 4; } fn get_taddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> SecretKey { @@ -172,6 +174,7 @@ impl LightWallet { = LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0); Ok(LightWallet { + locked: false, seed: seed_bytes, extsks: Arc::new(RwLock::new(vec![extsk])), extfvks: Arc::new(RwLock::new(vec![extfvk])), @@ -189,6 +192,12 @@ impl LightWallet { assert!(version <= LightWallet::serialized_version()); info!("Reading wallet version {}", version); + let locked = if version >= 4 { + reader.read_u8()? > 0 + } else { + false + }; + // Seed let mut seed_bytes = [0u8; 32]; reader.read_exact(&mut seed_bytes)?; @@ -196,9 +205,14 @@ impl LightWallet { // Read the spending keys let extsks = Vector::read(&mut reader, |r| ExtendedSpendingKey::read(r))?; - // Calculate the viewing keys - let extfvks = extsks.iter().map(|sk| ExtendedFullViewingKey::from(sk)) - .collect::>(); + let extfvks = if version >= 4 { + // Read the viewing keys + Vector::read(&mut reader, |r| ExtendedFullViewingKey::read(r))? + } else { + // Calculate the viewing keys + extsks.iter().map(|sk| ExtendedFullViewingKey::from(sk)) + .collect::>() + }; // Calculate the addresses let addresses = extfvks.iter().map( |fvk| fvk.default_address().unwrap().1 ) @@ -230,6 +244,7 @@ impl LightWallet { let birthday = reader.read_u64::()?; Ok(LightWallet{ + locked: locked, seed: seed_bytes, extsks: Arc::new(RwLock::new(extsks)), extfvks: Arc::new(RwLock::new(extfvks)), @@ -246,6 +261,9 @@ impl LightWallet { // Write the version writer.write_u64::(LightWallet::serialized_version())?; + // Write if it is locked + writer.write_u8(if self.locked {1} else {0})?; + // Write the seed writer.write_all(&self.seed)?; @@ -257,6 +275,11 @@ impl LightWallet { |w, sk| sk.write(w) )?; + // Write the FVKs + Vector::write(&mut writer, &self.extfvks.read().unwrap(), + |w, fvk| fvk.write(w) + )?; + // Write the transparent private key Vector::write(&mut writer, &self.tkeys.read().unwrap(), |w, pk| w.write_all(&pk[..]) From 201b47af5c89b3b6222d1188656bd5f828a61c4b Mon Sep 17 00:00:00 2001 From: Za Wilcox Date: Fri, 18 Oct 2019 20:28:02 -0600 Subject: [PATCH 06/24] make startup file creation fail more explicitly (#7) * add explicit permission error handling * create error_helpers as mod to keep main.rs succint * rename error_helpers -> startup_helpers --- lib/src/lib.rs | 1 + lib/src/startup_helpers.rs | 20 ++++++++++++++++++++ src/main.rs | 9 +++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 lib/src/startup_helpers.rs diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 3e4fe27..13672b7 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,6 +1,7 @@ #[macro_use] extern crate rust_embed; +pub mod startup_helpers; pub mod lightclient; pub mod grpcconnector; pub mod lightwallet; diff --git a/lib/src/startup_helpers.rs b/lib/src/startup_helpers.rs new file mode 100644 index 0000000..0b8e4bd --- /dev/null +++ b/lib/src/startup_helpers.rs @@ -0,0 +1,20 @@ +pub fn report_permission_error() { + let user = std::env::var("USER").expect( + "Unexpected error reading value of $USER!"); + let home = std::env::var("HOME").expect( + "Unexpected error reading value of $HOME!"); + let current_executable = std::env::current_exe() + .expect("Unexpected error reporting executable path!"); + eprintln!("USER: {}", user); + eprintln!("HOME: {}", home); + eprintln!("Executable: {}", current_executable.display()); + if home == "/" { + eprintln!("User {} must have permission to write to '{}.zcash/' .", + user, + home); + } else { + eprintln!("User {} must have permission to write to '{}/.zcash/' .", + user, + home); + } +} diff --git a/src/main.rs b/src/main.rs index 96fde21..d9b0ecc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::io::{Result, Error, ErrorKind}; use std::sync::Arc; use std::sync::mpsc::{channel, Sender, Receiver}; -use zecwalletlitelib::{commands, +use zecwalletlitelib::{commands, startup_helpers, lightclient::{self, LightClient, LightClientConfig}, }; @@ -111,12 +111,17 @@ pub fn main() { let dangerous = matches.is_present("dangerous"); let nosync = matches.is_present("nosync"); - let (command_tx, resp_rx) = match startup(server, dangerous, seed, !nosync, command.is_none()) { Ok(c) => c, Err(e) => { eprintln!("Error during startup: {}", e); error!("Error during startup: {}", e); + match e.raw_os_error() { + Some(13) => { + startup_helpers::report_permission_error(); + }, + _ => eprintln!("Something else!") + } return; } }; From fc15de5687045efb1359a0c01a5e608fc930fbc5 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 19 Oct 2019 11:18:22 -0700 Subject: [PATCH 07/24] Support mutable wallets --- lib/src/lightclient.rs | 233 ++++++++++++++++++++++------------------- lib/src/lightwallet.rs | 100 +++++++++++------- 2 files changed, 189 insertions(+), 144 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index b19f2f6..a242d63 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -179,7 +179,7 @@ impl LightClientConfig { } pub struct LightClient { - pub wallet : Arc, + pub wallet : Arc>, pub config : LightClientConfig, @@ -196,7 +196,7 @@ impl LightClient { let state = self.config.get_initial_state(); match state { - Some((height, hash, tree)) => self.wallet.set_initial_block(height.try_into().unwrap(), hash, tree), + Some((height, hash, tree)) => self.wallet.read().unwrap().set_initial_block(height.try_into().unwrap(), hash, tree), _ => true, }; } @@ -213,14 +213,14 @@ impl LightClient { let wallet = LightWallet::read(&mut file_buffer, config)?; LightClient { - wallet : Arc::new(wallet), + wallet : Arc::new(RwLock::new(wallet)), config : config.clone(), sapling_output : vec![], sapling_spend : vec![] } } else { let l = LightClient { - wallet : Arc::new(LightWallet::new(seed_phrase, config, latest_block)?), + wallet : Arc::new(RwLock::new(LightWallet::new(seed_phrase, config, latest_block)?)), config : config.clone(), sapling_output : vec![], sapling_spend : vec![] @@ -231,7 +231,7 @@ impl LightClient { l }; - info!("Read wallet with birthday {}", lc.wallet.get_first_tx_block()); + info!("Read wallet with birthday {}", lc.wallet.read().unwrap().get_first_tx_block()); // Read Sapling Params lc.sapling_output.extend_from_slice(SaplingParams::get("sapling-output.params").unwrap().as_ref()); @@ -243,16 +243,16 @@ impl LightClient { } pub fn last_scanned_height(&self) -> u64 { - self.wallet.last_scanned_height() as u64 + self.wallet.read().unwrap().last_scanned_height() as u64 } // Export private keys pub fn do_export(&self, addr: Option) -> JsonValue { // Clone address so it can be moved into the closure let address = addr.clone(); - + let wallet = self.wallet.read().unwrap(); // Go over all z addresses - let z_keys = self.wallet.get_z_private_keys().iter() + let z_keys = wallet.get_z_private_keys().iter() .filter( move |(addr, _)| address.is_none() || address.as_ref() == Some(addr)) .map( |(addr, pk)| object!{ @@ -265,7 +265,7 @@ impl LightClient { let address = addr.clone(); // Go over all t addresses - let t_keys = self.wallet.get_t_secret_keys().iter() + let t_keys = wallet.get_t_secret_keys().iter() .filter( move |(addr, _)| address.is_none() || address.as_ref() == Some(addr)) .map( |(addr, sk)| object!{ @@ -282,15 +282,15 @@ impl LightClient { } pub fn do_address(&self) -> JsonValue { + let wallet = self.wallet.read().unwrap(); // Collect z addresses - let z_addresses = self.wallet.address.read().unwrap().iter().map( |ad| { + let z_addresses = wallet.zaddress.read().unwrap().iter().map( |ad| { encode_payment_address(self.config.hrp_sapling_address(), &ad) }).collect::>(); // Collect t addresses - let t_addresses = self.wallet.tkeys.read().unwrap().iter().map( |sk| { - self.wallet.address_from_sk(&sk) - }).collect::>(); + let t_addresses = wallet.taddresses.read().unwrap().iter().map( |a| a.clone() ) + .collect::>(); object!{ "z_addresses" => z_addresses, @@ -299,33 +299,33 @@ impl LightClient { } pub fn do_balance(&self) -> JsonValue { + let wallet = self.wallet.read().unwrap(); + // Collect z addresses - let z_addresses = self.wallet.address.read().unwrap().iter().map( |ad| { + let z_addresses = wallet.zaddress.read().unwrap().iter().map( |ad| { let address = encode_payment_address(self.config.hrp_sapling_address(), &ad); object!{ "address" => address.clone(), - "zbalance" => self.wallet.zbalance(Some(address.clone())), - "verified_zbalance" => self.wallet.verified_zbalance(Some(address)), + "zbalance" => wallet.zbalance(Some(address.clone())), + "verified_zbalance" => wallet.verified_zbalance(Some(address)), } }).collect::>(); // Collect t addresses - let t_addresses = self.wallet.tkeys.read().unwrap().iter().map( |sk| { - let address = self.wallet.address_from_sk(&sk); - + let t_addresses = wallet.taddresses.read().unwrap().iter().map( |address| { // Get the balance for this address - let balance = self.wallet.tbalance(Some(address.clone())); + let balance = wallet.tbalance(Some(address.clone())); object!{ - "address" => address, + "address" => address.clone(), "balance" => balance, } }).collect::>(); object!{ - "zbalance" => self.wallet.zbalance(None), - "verified_zbalance" => self.wallet.verified_zbalance(None), - "tbalance" => self.wallet.tbalance(None), + "zbalance" => wallet.zbalance(None), + "verified_zbalance" => wallet.verified_zbalance(None), + "tbalance" => wallet.tbalance(None), "z_addresses" => z_addresses, "t_addresses" => t_addresses, } @@ -336,7 +336,7 @@ impl LightClient { 1_000_000, // 1 MB write buffer File::create(self.config.get_wallet_path()).unwrap()); - match self.wallet.write(&mut file_buffer) { + match self.wallet.write().unwrap().write(&mut file_buffer) { Ok(_) => { info!("Saved wallet"); let response = object!{ @@ -375,9 +375,10 @@ impl LightClient { } pub fn do_seed_phrase(&self) -> JsonValue { + let wallet = self.wallet.read().unwrap(); object!{ - "seed" => self.wallet.get_seed_phrase(), - "birthday" => self.wallet.get_birthday() + "seed" => wallet.get_seed_phrase(), + "birthday" => wallet.get_birthday() } } @@ -387,69 +388,75 @@ impl LightClient { let mut spent_notes : Vec = vec![]; let mut pending_notes: Vec = vec![]; - // Collect Sapling notes - self.wallet.txs.read().unwrap().iter() - .flat_map( |(txid, wtx)| { - wtx.notes.iter().filter_map(move |nd| - if !all_notes && nd.spent.is_some() { - None + { + // Collect Sapling notes + let wallet = self.wallet.read().unwrap(); + wallet.txs.read().unwrap().iter() + .flat_map( |(txid, wtx)| { + wtx.notes.iter().filter_map(move |nd| + if !all_notes && nd.spent.is_some() { + None + } else { + Some(object!{ + "created_in_block" => wtx.block, + "datetime" => wtx.datetime, + "created_in_txid" => format!("{}", txid), + "value" => nd.note.value, + "is_change" => nd.is_change, + "address" => LightWallet::note_address(self.config.hrp_sapling_address(), nd), + "spent" => nd.spent.map(|spent_txid| format!("{}", spent_txid)), + "unconfirmed_spent" => nd.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), + }) + } + ) + }) + .for_each( |note| { + if note["spent"].is_null() && note["unconfirmed_spent"].is_null() { + unspent_notes.push(note); + } else if !note["spent"].is_null() { + spent_notes.push(note); } else { - Some(object!{ - "created_in_block" => wtx.block, - "datetime" => wtx.datetime, - "created_in_txid" => format!("{}", txid), - "value" => nd.note.value, - "is_change" => nd.is_change, - "address" => self.wallet.note_address(nd), - "spent" => nd.spent.map(|spent_txid| format!("{}", spent_txid)), - "unconfirmed_spent" => nd.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), - }) + pending_notes.push(note); } - ) - }) - .for_each( |note| { - if note["spent"].is_null() && note["unconfirmed_spent"].is_null() { - unspent_notes.push(note); - } else if !note["spent"].is_null() { - spent_notes.push(note); - } else { - pending_notes.push(note); - } - }); + }); + } let mut unspent_utxos: Vec = vec![]; let mut spent_utxos : Vec = vec![]; let mut pending_utxos: Vec = vec![]; - - self.wallet.txs.read().unwrap().iter() - .flat_map( |(txid, wtx)| { - wtx.utxos.iter().filter_map(move |utxo| - if !all_notes && utxo.spent.is_some() { - None + + { + let wallet = self.wallet.read().unwrap(); + wallet.txs.read().unwrap().iter() + .flat_map( |(txid, wtx)| { + wtx.utxos.iter().filter_map(move |utxo| + if !all_notes && utxo.spent.is_some() { + None + } else { + Some(object!{ + "created_in_block" => wtx.block, + "datetime" => wtx.datetime, + "created_in_txid" => format!("{}", txid), + "value" => utxo.value, + "scriptkey" => hex::encode(utxo.script.clone()), + "is_change" => false, // TODO: Identify notes as change if we send change to taddrs + "address" => utxo.address.clone(), + "spent" => utxo.spent.map(|spent_txid| format!("{}", spent_txid)), + "unconfirmed_spent" => utxo.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), + }) + } + ) + }) + .for_each( |utxo| { + if utxo["spent"].is_null() && utxo["unconfirmed_spent"].is_null() { + unspent_utxos.push(utxo); + } else if !utxo["spent"].is_null() { + spent_utxos.push(utxo); } else { - Some(object!{ - "created_in_block" => wtx.block, - "datetime" => wtx.datetime, - "created_in_txid" => format!("{}", txid), - "value" => utxo.value, - "scriptkey" => hex::encode(utxo.script.clone()), - "is_change" => false, // TODO: Identify notes as change if we send change to taddrs - "address" => utxo.address.clone(), - "spent" => utxo.spent.map(|spent_txid| format!("{}", spent_txid)), - "unconfirmed_spent" => utxo.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), - }) + pending_utxos.push(utxo); } - ) - }) - .for_each( |utxo| { - if utxo["spent"].is_null() && utxo["unconfirmed_spent"].is_null() { - unspent_utxos.push(utxo); - } else if !utxo["spent"].is_null() { - spent_utxos.push(utxo); - } else { - pending_utxos.push(utxo); - } - }); + }); + } let mut res = object!{ "unspent_notes" => unspent_notes, @@ -467,8 +474,9 @@ impl LightClient { } pub fn do_list_transactions(&self) -> JsonValue { + let wallet = self.wallet.read().unwrap(); // Create a list of TransactionItems - let mut tx_list = self.wallet.txs.read().unwrap().iter() + let mut tx_list = wallet.txs.read().unwrap().iter() .flat_map(| (_k, v) | { let mut txns: Vec = vec![]; @@ -512,7 +520,7 @@ impl LightClient { "datetime" => v.datetime, "txid" => format!("{}", v.txid), "amount" => nd.note.value as i64, - "address" => self.wallet.note_address(nd), + "address" => LightWallet::note_address(self.config.hrp_sapling_address(), nd), "memo" => LightWallet::memo_str(&nd.memo), }) ); @@ -547,9 +555,11 @@ impl LightClient { /// Create a new address, deriving it from the seed. pub fn do_new_address(&self, addr_type: &str) -> JsonValue { + let wallet = self.wallet.write().unwrap(); + let new_address = match addr_type { - "z" => self.wallet.add_zaddr(), - "t" => self.wallet.add_taddr(), + "z" => wallet.add_zaddr(), + "t" => wallet.add_taddr(), _ => { let e = format!("Unrecognized address type: {}", addr_type); error!("{}", e); @@ -565,7 +575,7 @@ impl LightClient { pub fn do_rescan(&self) -> String { info!("Rescan starting"); // First, clear the state from the wallet - self.wallet.clear_blocks(); + self.wallet.read().unwrap().clear_blocks(); // Then set the initial block self.set_wallet_initial_state(); @@ -583,7 +593,7 @@ impl LightClient { // 2. Get all the blocks that we don't have // 3. Find all new Txns that don't have the full Tx, and get them as full transactions // and scan them, mainly to get the memos - let mut last_scanned_height = self.wallet.last_scanned_height() as u64; + let mut last_scanned_height = self.wallet.read().unwrap().last_scanned_height() as u64; // This will hold the latest block fetched from the RPC let latest_block_height = Arc::new(AtomicU64::new(0)); @@ -662,7 +672,7 @@ impl LightClient { Err(_) => {} } - match local_light_wallet.scan_block(encoded_block) { + match local_light_wallet.read().unwrap().scan_block(encoded_block) { Ok(block_txns) => { // Add to global tx list all_txs.write().unwrap().extend_from_slice(&block_txns.iter().map(|txid| (txid.clone(), height as i32)).collect::>()[..]); @@ -679,7 +689,7 @@ impl LightClient { // Check if there was any invalid block, which means we might have to do a reorg let invalid_height = last_invalid_height.load(Ordering::SeqCst); if invalid_height > 0 { - total_reorg += self.wallet.invalidate_block(invalid_height); + total_reorg += self.wallet.read().unwrap().invalidate_block(invalid_height); warn!("Invalidated block at height {}. Total reorg is now {}", invalid_height, total_reorg); } @@ -705,19 +715,28 @@ impl LightClient { total_reorg = 0; // We'll also fetch all the txids that our transparent addresses are involved with - // 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, self.config.no_cert_verification, - move |tx_bytes: &[u8], height: u64 | { - let tx = Transaction::read(tx_bytes).unwrap(); - - // Scan this Tx for transparent inputs and outputs - let datetime = block_times.read().unwrap().get(&height).map(|v| *v).unwrap_or(0); - wallet.scan_full_tx(&tx, height as i32, datetime as u64); + { + // Copy over addresses so as to not lock up the wallet, which we'll use inside the callback below. + let addresses = self.wallet.read().unwrap() + .taddresses.read().unwrap().iter().map(|a| a.clone()) + .collect::>(); + for address in addresses { + let wallet = self.wallet.clone(); + let block_times_inner = block_times.clone(); + + 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(); + + // Scan this Tx for transparent inputs and outputs + let datetime = block_times_inner.read().unwrap().get(&height).map(|v| *v).unwrap_or(0); + wallet.read().unwrap().scan_full_tx(&tx, height as i32, datetime as u64); + } + ); } - ); + } + // Do block height accounting last_scanned_height = end_height; end_height = last_scanned_height + 1000; @@ -741,10 +760,10 @@ 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 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::>(); + let mut txids_to_fetch: Vec<(TxId, i32)> = self.wallet.read().unwrap().txs.read().unwrap().values() + .filter(|wtx| wtx.full_tx_scanned == false) + .map(|wtx| (wtx.txid.clone(), wtx.block)) + .collect::>(); 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()[..]); @@ -763,7 +782,7 @@ impl LightClient { 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, 0); + light_wallet_clone.read().unwrap().scan_full_tx(&tx, height, 0); }); }; @@ -773,7 +792,7 @@ impl LightClient { pub fn do_send(&self, addrs: Vec<(&str, u64, Option)>) -> String { info!("Creating transaction"); - let rawtx = self.wallet.send_to_address( + let rawtx = self.wallet.write().unwrap().send_to_address( u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), &self.sapling_spend, &self.sapling_output, addrs diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 99e1698..d849440 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -99,10 +99,12 @@ pub struct LightWallet { extsks: Arc>>, extfvks: Arc>>, - pub address: Arc>>>, + pub zaddress: Arc>>>, - // Transparent keys. TODO: Make it not pubic - pub tkeys: Arc>>, + // Transparent keys. If the wallet is locked, then the secret keys will be encrypted, + // but the addresses will be present. + tkeys: Arc>>, + pub taddresses: Arc>>, blocks: Arc>>, pub txs: Arc>>, @@ -165,8 +167,9 @@ impl LightWallet { // we need to get the 64 byte bip39 entropy let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&seed_bytes, Language::English).unwrap(), ""); - // Derive only the first address + // Derive only the first sk and address let tpk = LightWallet::get_taddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0); + let taddr = LightWallet::address_from_prefix_sk(&config.base58_pubkey_address(), &tpk); // TODO: We need to monitor addresses, and always keep 1 "free" address, so // users can import a seed phrase and automatically get all used addresses @@ -174,16 +177,17 @@ impl LightWallet { = LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0); Ok(LightWallet { - locked: false, - seed: seed_bytes, - extsks: Arc::new(RwLock::new(vec![extsk])), - extfvks: Arc::new(RwLock::new(vec![extfvk])), - address: Arc::new(RwLock::new(vec![address])), - tkeys: Arc::new(RwLock::new(vec![tpk])), - blocks: Arc::new(RwLock::new(vec![])), - txs: Arc::new(RwLock::new(HashMap::new())), - config: config.clone(), - birthday: latest_block, + locked: false, + seed: seed_bytes, + extsks: Arc::new(RwLock::new(vec![extsk])), + extfvks: Arc::new(RwLock::new(vec![extfvk])), + zaddress: Arc::new(RwLock::new(vec![address])), + tkeys: Arc::new(RwLock::new(vec![tpk])), + taddresses: Arc::new(RwLock::new(vec![taddr])), + blocks: Arc::new(RwLock::new(vec![])), + txs: Arc::new(RwLock::new(HashMap::new())), + config: config.clone(), + birthday: latest_block, }) } @@ -224,6 +228,14 @@ impl LightWallet { secp256k1::SecretKey::from_slice(&tpk_bytes).map_err(|e| io::Error::new(ErrorKind::InvalidData, e)) })?; + let taddresses = if version >= 4 { + // Read the addresses + Vector::read(&mut reader, |r| utils::read_string(r))? + } else { + // Calculate the addresses + tkeys.iter().map(|sk| LightWallet::address_from_prefix_sk(&config.base58_pubkey_address(), sk)).collect() + }; + let blocks = Vector::read(&mut reader, |r| BlockData::read(r))?; let txs_tuples = Vector::read(&mut reader, |r| { @@ -244,15 +256,16 @@ impl LightWallet { let birthday = reader.read_u64::()?; Ok(LightWallet{ - locked: locked, - seed: seed_bytes, - extsks: Arc::new(RwLock::new(extsks)), - extfvks: Arc::new(RwLock::new(extfvks)), - address: Arc::new(RwLock::new(addresses)), - tkeys: Arc::new(RwLock::new(tkeys)), - blocks: Arc::new(RwLock::new(blocks)), - txs: Arc::new(RwLock::new(txs)), - config: config.clone(), + locked: locked, + seed: seed_bytes, + extsks: Arc::new(RwLock::new(extsks)), + extfvks: Arc::new(RwLock::new(extfvks)), + zaddress: Arc::new(RwLock::new(addresses)), + tkeys: Arc::new(RwLock::new(tkeys)), + taddresses: Arc::new(RwLock::new(taddresses)), + blocks: Arc::new(RwLock::new(blocks)), + txs: Arc::new(RwLock::new(txs)), + config: config.clone(), birthday, }) } @@ -280,11 +293,16 @@ impl LightWallet { |w, fvk| fvk.write(w) )?; - // Write the transparent private key + // Write the transparent private keys Vector::write(&mut writer, &self.tkeys.read().unwrap(), |w, pk| w.write_all(&pk[..]) )?; + // Write the transparent addresses + Vector::write(&mut writer, &self.taddresses.read().unwrap(), + |w, a| utils::write_string(w, a) + )?; + Vector::write(&mut writer, &self.blocks.read().unwrap(), |w, b| b.write(w))?; // The hashmap, write as a set of tuples @@ -302,9 +320,9 @@ impl LightWallet { Ok(()) } - pub fn note_address(&self, note: &SaplingNoteData) -> Option { + pub fn note_address(hrp: &str, note: &SaplingNoteData) -> Option { match note.extfvk.fvk.vk.into_payment_address(note.diversifier, &JUBJUB) { - Some(pa) => Some(encode_payment_address(self.config.hrp_sapling_address(), &pa)), + Some(pa) => Some(encode_payment_address(hrp, &pa)), None => None } } @@ -340,7 +358,8 @@ impl LightWallet { /// Get all t-address private keys. Returns a Vector of (address, secretkey) pub fn get_t_secret_keys(&self) -> Vec<(String, String)> { self.tkeys.read().unwrap().iter().map(|sk| { - (self.address_from_sk(sk), sk[..].to_base58check(&self.config.base58_secretkey_prefix(), &[0x01])) + (self.address_from_sk(sk), + sk[..].to_base58check(&self.config.base58_secretkey_prefix(), &[0x01])) }).collect::>() } @@ -355,7 +374,7 @@ impl LightWallet { let zaddr = encode_payment_address(self.config.hrp_sapling_address(), &address); self.extsks.write().unwrap().push(extsk); self.extfvks.write().unwrap().push(extfvk); - self.address.write().unwrap().push(address); + self.zaddress.write().unwrap().push(address); zaddr } @@ -365,11 +384,14 @@ impl LightWallet { /// NOTE: This is not rescan the wallet pub fn add_taddr(&self) -> String { let pos = self.tkeys.read().unwrap().len() as u32; + let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &self.seed, pos); + let address = self.address_from_sk(&sk); self.tkeys.write().unwrap().push(sk); + self.taddresses.write().unwrap().push(address.clone()); - self.address_from_sk(&sk) + address } /// Clears all the downloaded blocks and resets the state back to the initial block. @@ -476,7 +498,7 @@ impl LightWallet { } } - pub fn address_from_sk(&self, sk: &secp256k1::SecretKey) -> String { + pub fn address_from_prefix_sk(prefix: &[u8; 2], sk: &secp256k1::SecretKey) -> String { let secp = secp256k1::Secp256k1::new(); let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk); @@ -484,7 +506,11 @@ impl LightWallet { let mut hash160 = ripemd160::Ripemd160::new(); hash160.input(Sha256::digest(&pk.serialize()[..].to_vec())); - hash160.result().to_base58check(&self.config.base58_pubkey_address(), &[]) + hash160.result().to_base58check(prefix, &[]) + } + + pub fn address_from_sk(&self, sk: &secp256k1::SecretKey) -> String { + LightWallet::address_from_prefix_sk(&self.config.base58_pubkey_address(), sk) } pub fn address_from_pubkeyhash(&self, ta: Option) -> Option { @@ -699,8 +725,8 @@ impl LightWallet { // outgoing metadata // Collect our t-addresses - let wallet_taddrs = self.tkeys.read().unwrap().iter() - .map(|sk| self.address_from_sk(sk)) + let wallet_taddrs = self.taddresses.read().unwrap().iter() + .map(|a| a.clone()) .collect::>(); for vout in tx.vout.iter() { @@ -768,7 +794,7 @@ impl LightWallet { // First, collect all our z addresses, to check for change // Collect z addresses - let z_addresses = self.address.read().unwrap().iter().map( |ad| { + let z_addresses = self.zaddress.read().unwrap().iter().map( |ad| { encode_payment_address(self.config.hrp_sapling_address(), &ad) }).collect::>(); @@ -1870,7 +1896,7 @@ pub mod tests { assert_eq!(wallet.extsks.read().unwrap().len(), wallet2.extsks.read().unwrap().len()); assert_eq!(wallet.extsks.read().unwrap()[0], wallet2.extsks.read().unwrap()[0]); assert_eq!(wallet.extfvks.read().unwrap()[0], wallet2.extfvks.read().unwrap()[0]); - assert_eq!(wallet.address.read().unwrap()[0], wallet2.address.read().unwrap()[0]); + assert_eq!(wallet.zaddress.read().unwrap()[0], wallet2.zaddress.read().unwrap()[0]); assert_eq!(wallet.tkeys.read().unwrap().len(), wallet2.tkeys.read().unwrap().len()); assert_eq!(wallet.tkeys.read().unwrap()[0], wallet2.tkeys.read().unwrap()[0]); @@ -1939,7 +1965,7 @@ pub mod tests { assert_eq!(wallet2.tkeys.read().unwrap().len(), 2); assert_eq!(wallet2.extsks.read().unwrap().len(), 2); assert_eq!(wallet2.extfvks.read().unwrap().len(), 2); - assert_eq!(wallet2.address.read().unwrap().len(), 2); + assert_eq!(wallet2.zaddress.read().unwrap().len(), 2); assert_eq!(taddr1, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0])); assert_eq!(taddr2, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[1])); @@ -2359,7 +2385,7 @@ pub mod tests { assert_eq!(txs[&sent_txid].notes[0].extfvk, wallet.extfvks.read().unwrap()[0]); assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - fee); - assert_eq!(wallet.note_address(&txs[&sent_txid].notes[0]), Some(my_address)); + assert_eq!(LightWallet::note_address(wallet.config.hrp_sapling_address(), &txs[&sent_txid].notes[0]), Some(my_address)); assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[0].memo), Some(memo)); } } From 72548e077ed199426ab239980f953151c73fb059 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 19 Oct 2019 13:09:59 -0700 Subject: [PATCH 08/24] Add lock/unlock API --- lib/Cargo.toml | 1 + lib/src/lightwallet.rs | 219 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 214 insertions(+), 6 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index fbef09b..748da13 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -34,6 +34,7 @@ webpki-roots = "0.16.0" tower-h2 = { git = "https://github.com/tower-rs/tower-h2" } rust-embed = { version = "5.1.0", features = ["debug-embed"] } rand = "0.7.2" +sodiumoxide = "0.2.5" [dependencies.bellman] git = "https://github.com/adityapk00/librustzcash.git" diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index d849440..c5c2be2 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -90,9 +90,11 @@ impl ToBase58Check for [u8] { } pub struct LightWallet { - locked: bool, // Is the wallet's spending keys locked? + locked: bool, // Is the wallet's spending keys locked? + enc_seed: [u8; 48], // If locked, this contains the encrypted seed + nonce: Vec, // Nonce used to encrypt the wallet. - seed: [u8; 32], // Seed phrase for this wallet. If wallet is locked, this is encrypted + seed: [u8; 32], // Seed phrase for this wallet. If wallet is locked, this is 0 // List of keys, actually in this wallet. If the wallet is locked, the `extsks` will be // encrypted (but the fvks are not encrpyted) @@ -123,6 +125,8 @@ impl LightWallet { } fn get_taddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> SecretKey { + assert_eq!(bip39_seed.len(), 64); + let ext_t_key = ExtendedPrivKey::with_seed(bip39_seed).unwrap(); ext_t_key .derive_private_key(KeyIndex::hardened_from_normalize_index(44).unwrap()).unwrap() @@ -134,10 +138,12 @@ impl LightWallet { } - fn get_zaddr_from_bip39seed(config: &LightClientConfig, bip39seed: &[u8], pos: u32) -> + fn get_zaddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> (ExtendedSpendingKey, ExtendedFullViewingKey, PaymentAddress) { + assert_eq!(bip39_seed.len(), 64); + let extsk: ExtendedSpendingKey = ExtendedSpendingKey::from_path( - &ExtendedSpendingKey::master(bip39seed), + &ExtendedSpendingKey::master(bip39_seed), &[ ChildIndex::Hardened(32), ChildIndex::Hardened(config.get_coin_type()), @@ -178,6 +184,8 @@ impl LightWallet { Ok(LightWallet { locked: false, + enc_seed: [0u8; 48], + nonce: vec![], seed: seed_bytes, extsks: Arc::new(RwLock::new(vec![extsk])), extfvks: Arc::new(RwLock::new(vec![extfvk])), @@ -202,6 +210,17 @@ impl LightWallet { false }; + let mut enc_seed = [0u8; 48]; + if version >= 4 { + reader.read_exact(&mut enc_seed)?; + } + + let nonce = if version >= 4 { + Vector::read(&mut reader, |r| r.read_u8())? + } else { + vec![] + }; + // Seed let mut seed_bytes = [0u8; 32]; reader.read_exact(&mut seed_bytes)?; @@ -257,6 +276,8 @@ impl LightWallet { Ok(LightWallet{ locked: locked, + enc_seed: enc_seed, + nonce: nonce, seed: seed_bytes, extsks: Arc::new(RwLock::new(extsks)), extfvks: Arc::new(RwLock::new(extfvks)), @@ -277,6 +298,12 @@ impl LightWallet { // Write if it is locked writer.write_u8(if self.locked {1} else {0})?; + // Write the encrypted seed bytes + writer.write_all(&self.enc_seed)?; + + // Write the nonce + Vector::write(&mut writer, &self.nonce, |w, b| w.write_u8(*b))?; + // Write the seed writer.write_all(&self.seed)?; @@ -368,8 +395,10 @@ impl LightWallet { /// NOTE: This does NOT rescan pub fn add_zaddr(&self) -> String { let pos = self.extsks.read().unwrap().len() as u32; + let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), ""); + let (extsk, extfvk, address) = - LightWallet::get_zaddr_from_bip39seed(&self.config, &self.seed, pos); + LightWallet::get_zaddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos); let zaddr = encode_payment_address(self.config.hrp_sapling_address(), &address); self.extsks.write().unwrap().push(extsk); @@ -384,8 +413,9 @@ impl LightWallet { /// NOTE: This is not rescan the wallet pub fn add_taddr(&self) -> String { let pos = self.tkeys.read().unwrap().len() as u32; + let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), ""); - let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &self.seed, pos); + let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos); let address = self.address_from_sk(&sk); self.tkeys.write().unwrap().push(sk); @@ -531,6 +561,102 @@ impl LightWallet { ).unwrap().phrase().to_string() } + pub fn lock(&mut self, passwd: String) -> io::Result<()> { + use sodiumoxide::crypto::secretbox; + + if self.locked { + return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is already locked")); + } + + // Get the doublesha256 of the password, which is the right length + let key = secretbox::Key::from_slice(&double_sha256(passwd.as_bytes())).unwrap(); + let nonce = secretbox::gen_nonce(); + + let cipher = secretbox::seal(&self.seed, &nonce, &key); + + self.enc_seed.copy_from_slice(&cipher); + self.nonce = vec![]; + self.nonce.extend_from_slice(nonce.as_ref()); + + // Empty the seed and the secret keys + self.seed.copy_from_slice(&[0u8; 32]); + self.tkeys = Arc::new(RwLock::new(vec![])); + self.extsks = Arc::new(RwLock::new(vec![])); + + self.locked = true; + Ok(()) + } + + pub fn unlock(&mut self, passwd: String) -> io::Result<()> { + use sodiumoxide::crypto::secretbox; + + if !self.locked { + return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is not locked")); + } + + // Get the doublesha256 of the password, which is the right length + let key = secretbox::Key::from_slice(&double_sha256(passwd.as_bytes())).unwrap(); + let nonce = secretbox::Nonce::from_slice(&self.nonce).unwrap(); + + let seed = match secretbox::open(&self.enc_seed, &nonce, &key) { + Ok(s) => s, + Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));} + }; + + // Now that we have the seed, we'll generate the extsks and tkeys, and verify the fvks and addresses + // respectively match + + // The seed bytes is the raw entropy. To pass it to HD wallet generation, + // we need to get the 64 byte bip39 entropy + let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&seed, Language::English).unwrap(), ""); + + // Sapling keys + let mut extsks = vec![]; + for pos in 0..self.zaddress.read().unwrap().len() { + let (extsk, extfvk, address) = + LightWallet::get_zaddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos as u32); + + if address != self.zaddress.read().unwrap()[pos] { + return Err(io::Error::new(ErrorKind::InvalidData, + format!("zaddress mismatch at {}. {:?} vs {:?}", pos, address, self.zaddress.read().unwrap()[pos]))); + } + + if extfvk != self.extfvks.read().unwrap()[pos] { + return Err(io::Error::new(ErrorKind::InvalidData, + format!("fvk mismatch at {}. {:?} vs {:?}", pos, extfvk, self.extfvks.read().unwrap()[pos]))); + } + + + // Don't add it to self yet, we'll do that at the end when everything is verified + extsks.push(extsk); + } + + // Transparent keys + let mut tkeys = vec![]; + for pos in 0..self.taddresses.read().unwrap().len() { + let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos as u32); + let address = self.address_from_sk(&sk); + + if address != self.taddresses.read().unwrap()[pos] { + return Err(io::Error::new(ErrorKind::InvalidData, + format!("taddress mismatch at {}. {} vs {}", pos, address, self.taddresses.read().unwrap()[pos]))); + } + + tkeys.push(sk); + } + + // Everything checks out, so we'll update our wallet with the unlocked values + self.extsks = Arc::new(RwLock::new(extsks)); + self.tkeys = Arc::new(RwLock::new(tkeys)); + self.seed.copy_from_slice(&seed); + + self.nonce = vec![]; + self.enc_seed.copy_from_slice(&[0u8; 48]); + self.locked = false; + + Ok(()) + } + pub fn zbalance(&self, addr: Option) -> u64 { self.txs.read().unwrap() .values() @@ -2927,6 +3053,87 @@ pub mod tests { assert_eq!(seed_phrase, Some(wallet.get_seed_phrase())); } + #[test] + fn test_lock_unlock() { + const AMOUNT: u64 = 500000; + + let (mut wallet, _, _) = get_test_wallet(AMOUNT); + let config = wallet.config.clone(); + + // Add some addresses + let zaddr0 = encode_payment_address(config.hrp_sapling_address(), + &wallet.extfvks.read().unwrap()[0].default_address().unwrap().1); + let zaddr1 = wallet.add_zaddr(); + let zaddr2 = wallet.add_zaddr(); + + let taddr0 = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); + let taddr1 = wallet.add_taddr(); + let taddr2 = wallet.add_taddr(); + + let seed = wallet.seed; + + wallet.lock("somepassword".to_string()).unwrap(); + + // Locking a locked wallet should fail + assert!(wallet.lock("somepassword".to_string()).is_err()); + + // Serialize a locked wallet + let mut serialized_data = vec![]; + wallet.write(&mut serialized_data).expect("Serialize wallet"); + + // Should fail when there's a wrong password + assert!(wallet.unlock("differentpassword".to_string()).is_err()); + + // Properly unlock + wallet.unlock("somepassword".to_string()).unwrap(); + + assert_eq!(seed, wallet.seed); + { + let extsks = wallet.extsks.read().unwrap(); + let tkeys = wallet.tkeys.read().unwrap(); + assert_eq!(extsks.len(), 3); + assert_eq!(tkeys.len(), 3); + + assert_eq!(zaddr0, encode_payment_address(config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[0]).default_address().unwrap().1)); + assert_eq!(zaddr1, encode_payment_address(config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[1]).default_address().unwrap().1)); + assert_eq!(zaddr2, encode_payment_address(config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[2]).default_address().unwrap().1)); + + assert_eq!(taddr0, wallet.address_from_sk(&tkeys[0])); + assert_eq!(taddr1, wallet.address_from_sk(&tkeys[1])); + assert_eq!(taddr2, wallet.address_from_sk(&tkeys[2])); + } + + // Unlocking an already unlocked wallet should fail + assert!(wallet.unlock("somepassword".to_string()).is_err()); + + + // Try from a deserialized, locked wallet + let mut wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap(); + wallet2.unlock("somepassword".to_string()).unwrap(); + + assert_eq!(seed, wallet2.seed); + { + let extsks = wallet2.extsks.read().unwrap(); + let tkeys = wallet2.tkeys.read().unwrap(); + assert_eq!(extsks.len(), 3); + assert_eq!(tkeys.len(), 3); + + assert_eq!(zaddr0, encode_payment_address(wallet2.config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[0]).default_address().unwrap().1)); + assert_eq!(zaddr1, encode_payment_address(wallet2.config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[1]).default_address().unwrap().1)); + assert_eq!(zaddr2, encode_payment_address(wallet2.config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[2]).default_address().unwrap().1)); + + assert_eq!(taddr0, wallet2.address_from_sk(&tkeys[0])); + assert_eq!(taddr1, wallet2.address_from_sk(&tkeys[1])); + assert_eq!(taddr2, wallet2.address_from_sk(&tkeys[2])); + } + } + #[test] fn test_invalid_scan_blocks() { const AMOUNT: u64 = 500000; From 89a5fc0ea1293292048775794d10c855ba34f8be Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 19 Oct 2019 15:55:24 -0700 Subject: [PATCH 09/24] Tests for derivation bugfix --- lib/src/lightwallet.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index c5c2be2..5d76af7 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -3046,6 +3046,12 @@ pub mod tests { assert_eq!(taddr, "t1eQ63fwkQ4n4Eo5uCrPGaAV8FWB2tmx7ui"); assert_eq!(pk, "Kz9ybX4giKag4NtnP1pi8WQF2B2hZDkFU85S7Dciz3UUhM59AnhE"); + // Test a couple more + wallet.add_taddr(); + let (taddr, pk) = &wallet.get_t_secret_keys()[1]; + assert_eq!(taddr, "t1NoS6ZgaUTpmjkge2cVpXGcySasdYDrXqh"); + assert_eq!(pk, "KxdmS38pxskS6bbKX43zhTu8ppWckNmWjKsQFX1hwidvhRRgRd3c"); + let (zaddr, sk) = &wallet.get_z_private_keys()[0]; assert_eq!(zaddr, "zs1q6xk3q783t5k92kjqt2rkuuww8pdw2euzy5rk6jytw97enx8fhpazdv3th4xe7vsk6e9sfpawfg"); assert_eq!(sk, "secret-extended-key-main1qvpa0qr8qqqqpqxn4l054nzxpxzp3a8r2djc7sekdek5upce8mc2j2z0arzps4zv940qeg706hd0wq6g5snzvhp332y6vhwyukdn8dhekmmsk7fzvzkqm6ypc99uy63tpesqwxhpre78v06cx8k5xpp9mrhtgqs5dvp68cqx2yrvthflmm2ynl8c0506dekul0f6jkcdmh0292lpphrksyc5z3pxwws97zd5els3l2mjt2s7hntap27mlmt6w0drtfmz36vz8pgu7ec0twfrq"); @@ -3134,6 +3140,22 @@ pub mod tests { } } + #[test] + #[should_panic] + fn test_invalid_bip39_t() { + // Passing a 32-byte seed to bip32 should fail. + let config = get_test_config(); + LightWallet::get_taddr_from_bip39seed(&config, &[0u8; 32], 0); + } + + #[test] + #[should_panic] + fn test_invalid_bip39_z() { + // Passing a 32-byte seed to bip32 should fail. + let config = get_test_config(); + LightWallet::get_zaddr_from_bip39seed(&config, &[0u8; 32], 0); + } + #[test] fn test_invalid_scan_blocks() { const AMOUNT: u64 = 500000; From 3ebc46185c03e80d698ad03c5cede065df686ee1 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 19 Oct 2019 16:23:32 -0700 Subject: [PATCH 10/24] Add bug detection --- lib/src/commands.rs | 65 ++++++++++++++++++++++++++++--------- lib/src/lightwallet.rs | 2 +- lib/src/lightwallet/bugs.rs | 40 +++++++++++++++++++++++ 3 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 lib/src/lightwallet/bugs.rs diff --git a/lib/src/commands.rs b/lib/src/commands.rs index 7f4b2de..7c57b7c 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -444,6 +444,40 @@ impl Command for NotesCommand { } } +struct FixBip39BugCommand {} +impl Command for FixBip39BugCommand { + fn help(&self) -> String { + let mut h = vec![]; + h.push("Detect if the wallet has the Bip39 derivation bug, and fix it automatically"); + h.push("Usage:"); + h.push("fixbip39bug"); + h.push(""); + + h.join("\n") + } + + fn short_help(&self) -> String { + "Detect if the wallet has the Bip39 derivation bug, and fix it automatically".to_string() + } + + fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { + use crate::lightwallet::bugs::BugBip39Derivation; + + let r = if BugBip39Derivation::has_bug(&lightclient.wallet.read().unwrap()) { + object!{ + "has_bug" => true, + "fixed" => false, + } + } else { + object!{ + "has_bug" => false, + "fixed" => false, + } + }; + + r.pretty(2) + } +} struct QuitCommand {} impl Command for QuitCommand { @@ -469,21 +503,22 @@ impl Command for QuitCommand { pub fn get_commands() -> Box>> { let mut map: HashMap> = HashMap::new(); - map.insert("sync".to_string(), Box::new(SyncCommand{})); - map.insert("rescan".to_string(), Box::new(RescanCommand{})); - 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{})); - map.insert("save".to_string(), Box::new(SaveCommand{})); - map.insert("quit".to_string(), Box::new(QuitCommand{})); - map.insert("list".to_string(), Box::new(TransactionsCommand{})); - map.insert("notes".to_string(), Box::new(NotesCommand{})); - map.insert("new".to_string(), Box::new(NewAddressCommand{})); - map.insert("seed".to_string(), Box::new(SeedCommand{})); + map.insert("sync".to_string(), Box::new(SyncCommand{})); + map.insert("rescan".to_string(), Box::new(RescanCommand{})); + 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{})); + map.insert("save".to_string(), Box::new(SaveCommand{})); + map.insert("quit".to_string(), Box::new(QuitCommand{})); + map.insert("list".to_string(), Box::new(TransactionsCommand{})); + map.insert("notes".to_string(), Box::new(NotesCommand{})); + map.insert("new".to_string(), Box::new(NewAddressCommand{})); + map.insert("seed".to_string(), Box::new(SeedCommand{})); + map.insert("fixbip39bug".to_string(), Box::new(FixBip39BugCommand{})); Box::new(map) } diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 5d76af7..e29a12e 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -47,6 +47,7 @@ mod extended_key; mod utils; mod address; mod prover; +pub mod bugs; use data::{BlockData, WalletTx, Utxo, SaplingNoteData, SpendableNote, OutgoingTxMetadata}; use extended_key::{KeyIndex, ExtendedPrivKey}; @@ -626,7 +627,6 @@ impl LightWallet { format!("fvk mismatch at {}. {:?} vs {:?}", pos, extfvk, self.extfvks.read().unwrap()[pos]))); } - // Don't add it to self yet, we'll do that at the end when everything is verified extsks.push(extsk); } diff --git a/lib/src/lightwallet/bugs.rs b/lib/src/lightwallet/bugs.rs new file mode 100644 index 0000000..52dc82f --- /dev/null +++ b/lib/src/lightwallet/bugs.rs @@ -0,0 +1,40 @@ +use super::LightWallet; + +use bip39::{Mnemonic, Language}; + +pub struct BugBip39Derivation {} + +impl BugBip39Derivation { + + pub fn has_bug(wallet: &LightWallet) -> bool { + if wallet.zaddress.read().unwrap().len() <= 1 { + return false; + } + + // The seed bytes is the raw entropy. To pass it to HD wallet generation, + // we need to get the 64 byte bip39 entropy + let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&wallet.seed, Language::English).unwrap(), ""); + + // Check z addresses + for pos in 0..wallet.zaddress.read().unwrap().len() { + let (_, _, address) = + LightWallet::get_zaddr_from_bip39seed(&wallet.config, &bip39_seed.as_bytes(), pos as u32); + + if address != wallet.zaddress.read().unwrap()[pos] { + return true; + } + } + + // Check t addresses + for pos in 0..wallet.taddresses.read().unwrap().len() { + let sk = LightWallet::get_taddr_from_bip39seed(&wallet.config, &bip39_seed.as_bytes(), pos as u32); + let address = wallet.address_from_sk(&sk); + + if address != wallet.taddresses.read().unwrap()[pos] { + return true; + } + } + + false + } +} \ No newline at end of file From 611974c5fa530b66ac4d4dd0aa684d9273efca78 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 19 Oct 2019 16:41:27 -0700 Subject: [PATCH 11/24] Add bugfix address derivation --- lib/src/commands.rs | 14 +----------- lib/src/lightwallet/bugs.rs | 45 ++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/lib/src/commands.rs b/lib/src/commands.rs index 7c57b7c..f529753 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -463,19 +463,7 @@ impl Command for FixBip39BugCommand { fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { use crate::lightwallet::bugs::BugBip39Derivation; - let r = if BugBip39Derivation::has_bug(&lightclient.wallet.read().unwrap()) { - object!{ - "has_bug" => true, - "fixed" => false, - } - } else { - object!{ - "has_bug" => false, - "fixed" => false, - } - }; - - r.pretty(2) + BugBip39Derivation::fix_bug(lightclient) } } diff --git a/lib/src/lightwallet/bugs.rs b/lib/src/lightwallet/bugs.rs index 52dc82f..0f6028d 100644 --- a/lib/src/lightwallet/bugs.rs +++ b/lib/src/lightwallet/bugs.rs @@ -1,12 +1,16 @@ use super::LightWallet; +use crate::lightclient::LightClient; +use json::object; use bip39::{Mnemonic, Language}; pub struct BugBip39Derivation {} impl BugBip39Derivation { - pub fn has_bug(wallet: &LightWallet) -> bool { + pub fn has_bug(client: &LightClient) -> bool { + let wallet = client.wallet.read().unwrap(); + if wallet.zaddress.read().unwrap().len() <= 1 { return false; } @@ -37,4 +41,43 @@ impl BugBip39Derivation { false } + + pub fn fix_bug(client: &LightClient) -> String { + if !BugBip39Derivation::has_bug(client) { + let r = object!{ + "has_bug" => false + }; + + return r.pretty(2); + } + + // TODO: Tranfer money + + // regen addresses + let wallet = client.wallet.read().unwrap(); + let num_zaddrs = wallet.zaddress.read().unwrap().len(); + let num_taddrs = wallet.taddresses.read().unwrap().len(); + + wallet.extsks.write().unwrap().truncate(1); + wallet.extfvks.write().unwrap().truncate(1); + wallet.zaddress.write().unwrap().truncate(1); + + wallet.tkeys.write().unwrap().truncate(1); + wallet.taddresses.write().unwrap().truncate(1); + + for _ in 1..num_zaddrs { + wallet.add_zaddr(); + } + + for _ in 1..num_taddrs { + wallet.add_taddr(); + } + + let r = object!{ + "has_bug" => true, + "fixed" => true, + }; + + return r.pretty(2); + } } \ No newline at end of file From 0e529ba4cdd3d225ae842a2d81c7a16514a35433 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 19 Oct 2019 20:21:34 -0700 Subject: [PATCH 12/24] Fix crash when insufficient funds --- lib/src/lightwallet/data.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/lightwallet/data.rs b/lib/src/lightwallet/data.rs index 8660f5d..38128ca 100644 --- a/lib/src/lightwallet/data.rs +++ b/lib/src/lightwallet/data.rs @@ -490,7 +490,8 @@ pub struct SpendableNote { impl SpendableNote { pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &ExtendedSpendingKey) -> Option { // Include only notes that haven't been spent, or haven't been included in an unconfirmed spend yet. - if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { + if nd.spent.is_none() && nd.unconfirmed_spent.is_none() && + nd.witnesses.len() >= (anchor_offset + 1) { let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1); witness.map(|w| SpendableNote { From 19d9dfa18ed6840b69909d4418f99e2b476ecac1 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 19 Oct 2019 20:21:53 -0700 Subject: [PATCH 13/24] Return results instead of JSON --- lib/src/commands.rs | 14 ++++++++++---- lib/src/lightclient.rs | 9 +++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/src/commands.rs b/lib/src/commands.rs index f529753..f6cca28 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -265,7 +265,10 @@ impl Command for SendCommand { }; lightclient.do_sync(true); - return lightclient.do_send(send_args); + match lightclient.do_send(send_args) { + Ok(txid) => { object!{ "txid" => txid } }, + Err(e) => { object!{ "error" => e } } + }.pretty(2) } else if args.len() == 2 || args.len() == 3 { // Make sure we can parse the amount let value = match args[1].parse::() { @@ -278,10 +281,13 @@ impl Command for SendCommand { let memo = if args.len() == 3 { Some(args[2].to_string()) } else {None}; lightclient.do_sync(true); - return lightclient.do_send(vec!((args[0], value, memo))); + match lightclient.do_send(vec!((args[0], value, memo))) { + Ok(txid) => { object!{ "txid" => txid } }, + Err(e) => { object!{ "error" => e } } + }.pretty(2) + } else { + self.help() } - - self.help() } } diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index a242d63..fb4ef62 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -789,7 +789,7 @@ impl LightClient { responses.join("\n") } - pub fn do_send(&self, addrs: Vec<(&str, u64, Option)>) -> String { + pub fn do_send(&self, addrs: Vec<(&str, u64, Option)>) -> Result { info!("Creating transaction"); let rawtx = self.wallet.write().unwrap().send_to_address( @@ -799,11 +799,8 @@ impl LightClient { ); match rawtx { - Ok(txbytes) => match broadcast_raw_tx(&self.get_server_uri(), self.config.no_cert_verification, txbytes) { - Ok(k) => k, - Err(e) => e, - }, - Err(e) => format!("Error: No Tx to broadcast. Error was: {}", e) + Ok(txbytes) => broadcast_raw_tx(&self.get_server_uri(), self.config.no_cert_verification, txbytes), + Err(e) => Err(format!("Error: No Tx to broadcast. Error was: {}", e)) } } } From 53713f5f2d4aee26d60938ae4d0847d1537b46f3 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 19 Oct 2019 20:22:03 -0700 Subject: [PATCH 14/24] Don't return json from GRPC --- lib/src/grpcconnector.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/src/grpcconnector.rs b/lib/src/grpcconnector.rs index 0f22c67..3a63b4e 100644 --- a/lib/src/grpcconnector.rs +++ b/lib/src/grpcconnector.rs @@ -4,8 +4,6 @@ use std::sync::{Arc}; use std::net::ToSocketAddrs; use std::net::SocketAddr; -use json::object; - use futures::{Future}; use futures::stream::Stream; @@ -286,11 +284,7 @@ pub fn broadcast_raw_tx(uri: &http::Uri, no_cert: bool, tx_bytes: Box<[u8]>) -> txid = txid[1..txid.len()-1].to_string(); } - let r = object!{ - "result" => "success", - "txid" => txid, - }; - Ok(r.pretty(2)) + Ok(txid) } else { Err(format!("Error: {:?}", sendresponse)) } From c3af5a1ca2774386d2c5ff8af64015f537e49b62 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 19 Oct 2019 20:22:19 -0700 Subject: [PATCH 15/24] Autofix for bip39bug --- lib/src/lightwallet.rs | 9 +++++++-- lib/src/lightwallet/bugs.rs | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index e29a12e..1c17c65 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -202,7 +202,12 @@ impl LightWallet { pub fn read(mut reader: R, config: &LightClientConfig) -> io::Result { let version = reader.read_u64::()?; - assert!(version <= LightWallet::serialized_version()); + if version > LightWallet::serialized_version() { + let e = format!("Don't know how to read wallet version {}. Do you have the latest version?", version); + error!("{}", e); + return Err(io::Error::new(ErrorKind::InvalidData, e)); + } + info!("Reading wallet version {}", version); let locked = if version >= 4 { @@ -1350,7 +1355,7 @@ impl LightWallet { if selected_value < u64::from(target_value) { let e = format!( - "Insufficient verified funds (have {}, need {:?}).\nNote: funds need {} confirmations before they can be spent", + "Insufficient verified funds (have {}, need {:?}). NOTE: funds need {} confirmations before they can be spent.", selected_value, target_value, self.config.anchor_offset ); error!("{}", e); diff --git a/lib/src/lightwallet/bugs.rs b/lib/src/lightwallet/bugs.rs index 0f6028d..fbe9755 100644 --- a/lib/src/lightwallet/bugs.rs +++ b/lib/src/lightwallet/bugs.rs @@ -43,6 +43,9 @@ impl BugBip39Derivation { } pub fn fix_bug(client: &LightClient) -> String { + use zcash_primitives::transaction::components::amount::DEFAULT_FEE; + use std::convert::TryInto; + if !BugBip39Derivation::has_bug(client) { let r = object!{ "has_bug" => false @@ -51,7 +54,32 @@ impl BugBip39Derivation { return r.pretty(2); } - // TODO: Tranfer money + // Tranfer money + // 1. The desination is z address #0 + let zaddr = client.do_address()["z_addresses"][0].as_str().unwrap().to_string(); + let balance_json = client.do_balance(); + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + let amount: u64 = balance_json["zbalance"].as_u64().unwrap() + + balance_json["tbalance"].as_u64().unwrap() + - fee; + + let txid = if amount > 0 { + match client.do_send(vec![(&zaddr, amount, None)]) { + Ok(txid) => txid, + Err(e) => { + let r = object!{ + "has_bug" => true, + "fixed" => false, + "error" => e, + }; + + return r.pretty(2); + } + } + } else { + "".to_string() + }; + // regen addresses let wallet = client.wallet.read().unwrap(); @@ -75,7 +103,8 @@ impl BugBip39Derivation { let r = object!{ "has_bug" => true, - "fixed" => true, + "fixed" => true, + "txid" => txid, }; return r.pretty(2); From e4f00a78d572bf25d892d32a6e8f9027051a7a6c Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 19 Oct 2019 20:29:17 -0700 Subject: [PATCH 16/24] Fix save command return value --- lib/src/commands.rs | 19 +++++++++++++++++-- lib/src/lightclient.rs | 12 +++--------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/src/commands.rs b/lib/src/commands.rs index f6cca28..6cc6f61 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -310,7 +310,19 @@ impl Command for SaveCommand { } fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { - lightclient.do_save() + match lightclient.do_save() { + Ok(_) => { + let r = object!{ "result" => "success" }; + r.pretty(2) + }, + Err(e) => { + let r = object!{ + "result" => "error", + "error" => e + }; + r.pretty(2) + } + } } } @@ -490,7 +502,10 @@ impl Command for QuitCommand { } fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { - lightclient.do_save() + match lightclient.do_save() { + Ok(_) => {"".to_string()}, + Err(e) => e + } } } diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index fb4ef62..b43e552 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -331,23 +331,17 @@ impl LightClient { } } - pub fn do_save(&self) -> String { + pub fn do_save(&self) -> Result<(), String> { let mut file_buffer = BufWriter::with_capacity( 1_000_000, // 1 MB write buffer File::create(self.config.get_wallet_path()).unwrap()); match self.wallet.write().unwrap().write(&mut file_buffer) { - Ok(_) => { - info!("Saved wallet"); - let response = object!{ - "result" => "success" - }; - response.pretty(2) - }, + Ok(_) => Ok(()), Err(e) => { let err = format!("ERR: {}", e); error!("{}", err); - err + Err(e.to_string()) } } } From 6607ecdc09a8466063c8e3db24da44c33ad8ff02 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 19 Oct 2019 22:12:45 -0700 Subject: [PATCH 17/24] Add comment --- lib/src/lightwallet/bugs.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/src/lightwallet/bugs.rs b/lib/src/lightwallet/bugs.rs index fbe9755..60f237f 100644 --- a/lib/src/lightwallet/bugs.rs +++ b/lib/src/lightwallet/bugs.rs @@ -1,3 +1,15 @@ +/// +/// In v1.0 of zecwallet-cli, there was a bug that incorrectly derived HD wallet keys after the first key. That is, the +/// first key, address was correct, but subsequent ones were not. +/// +/// The issue was that the 32-byte seed was directly being used to derive then subsequent addresses instead of the +/// 64-byte pkdf2(seed). The issue affected both t and z addresses +/// +/// To fix the bug, we need to: +/// 1. Check if the wallet has more than 1 address for t or z addresses +/// 2. Move any funds in these addresses to the first address +/// 3. Re-derive the addresses + use super::LightWallet; use crate::lightclient::LightClient; @@ -8,6 +20,7 @@ pub struct BugBip39Derivation {} impl BugBip39Derivation { + /// Check if this bug exists in the wallet pub fn has_bug(client: &LightClient) -> bool { let wallet = client.wallet.read().unwrap(); @@ -42,6 +55,7 @@ impl BugBip39Derivation { false } + /// Automatically fix the bug if it exists in the wallet pub fn fix_bug(client: &LightClient) -> String { use zcash_primitives::transaction::components::amount::DEFAULT_FEE; use std::convert::TryInto; From 8ade7caa48268745fc40926c10b5adf21f7f83e6 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sun, 20 Oct 2019 11:40:25 -0700 Subject: [PATCH 18/24] Wallet encryption commands --- lib/src/commands.rs | 121 +++++++++++++++++++++++++++++++++++++++++ lib/src/lightclient.rs | 42 ++++++++++++++ lib/src/lightwallet.rs | 109 +++++++++++++++++++++++++++++++------ 3 files changed, 255 insertions(+), 17 deletions(-) diff --git a/lib/src/commands.rs b/lib/src/commands.rs index 6cc6f61..98dbd4d 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -201,6 +201,124 @@ impl Command for ExportCommand { } } +struct EncryptCommand {} +impl Command for EncryptCommand { + fn help(&self) -> String { + let mut h = vec![]; + h.push("Encrypt the wallet with a password"); + h.push("Note 1: This will encrypt the seed and the sapling and transparent private keys."); + h.push(" Use 'unlock' to temporarily unlock the wallet for spending or 'decrypt' "); + h.push(" to permanatly remove the encryption"); + h.push("Note 2: If you forget the password, the only way to recover the wallet is to restore"); + h.push(" from the seed phrase."); + h.push("Usage:"); + h.push("encrypt password"); + h.push(""); + h.push("Example:"); + h.push("encrypt my_strong_password"); + + h.join("\n") + } + + fn short_help(&self) -> String { + "Encrypt the wallet with a password".to_string() + } + + fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { + if args.len() != 1 { + return self.help(); + } + + let passwd = args[0].to_string(); + + match lightclient.wallet.write().unwrap().encrypt(passwd) { + Ok(_) => object!{ "result" => "success" }, + Err(e) => object!{ + "result" => "error", + "error" => e.to_string() + } + }.pretty(2) + } +} + +struct DecryptCommand {} +impl Command for DecryptCommand { + fn help(&self) -> String { + let mut h = vec![]; + h.push("Completely remove wallet encryption, storing the wallet in plaintext on disk"); + h.push("Note 1: This will decrypt the seed and the sapling and transparent private keys and store them on disk."); + h.push(" Use 'unlock' to temporarily unlock the wallet for spending"); + h.push("Note 2: If you've forgotten the password, the only way to recover the wallet is to restore"); + h.push(" from the seed phrase."); + h.push("Usage:"); + h.push("decrypt password"); + h.push(""); + h.push("Example:"); + h.push("decrypt my_strong_password"); + + h.join("\n") + } + + fn short_help(&self) -> String { + "Completely remove wallet encryption".to_string() + } + + fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { + if args.len() != 1 { + return self.help(); + } + + let passwd = args[0].to_string(); + + match lightclient.wallet.write().unwrap().remove_encryption(passwd) { + Ok(_) => object!{ "result" => "success" }, + Err(e) => object!{ + "result" => "error", + "error" => e.to_string() + } + }.pretty(2) + } +} + + +struct UnlockCommand {} +impl Command for UnlockCommand { + fn help(&self) -> String { + let mut h = vec![]; + h.push("Unlock the wallet's encryption in memory, allowing spending from this wallet."); + h.push("Note 1: This will decrypt spending keys in memory only. The wallet remains encrypted on disk"); + h.push(" Use 'decrypt' to remove the encryption permanatly."); + h.push("Note 2: If you've forgotten the password, the only way to recover the wallet is to restore"); + h.push(" from the seed phrase."); + h.push("Usage:"); + h.push("unlock password"); + h.push(""); + h.push("Example:"); + h.push("unlock my_strong_password"); + + h.join("\n") + } + + fn short_help(&self) -> String { + "Unlock wallet encryption for spending".to_string() + } + + fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { + if args.len() != 1 { + return self.help(); + } + + let passwd = args[0].to_string(); + + match lightclient.wallet.write().unwrap().unlock(passwd) { + Ok(_) => object!{ "result" => "success" }, + Err(e) => object!{ + "result" => "error", + "error" => e.to_string() + } + }.pretty(2) + } +} struct SendCommand {} impl Command for SendCommand { @@ -527,6 +645,9 @@ pub fn get_commands() -> Box>> { map.insert("notes".to_string(), Box::new(NotesCommand{})); map.insert("new".to_string(), Box::new(NewAddressCommand{})); map.insert("seed".to_string(), Box::new(SeedCommand{})); + map.insert("encrypt".to_string(), Box::new(EncryptCommand{})); + map.insert("decrypt".to_string(), Box::new(DecryptCommand{})); + map.insert("unlock".to_string(), Box::new(UnlockCommand{})); map.insert("fixbip39bug".to_string(), Box::new(FixBip39BugCommand{})); Box::new(map) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index b43e552..fa51cad 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -248,6 +248,13 @@ impl LightClient { // Export private keys pub fn do_export(&self, addr: Option) -> JsonValue { + if !self.wallet.read().unwrap().is_unlocked_for_spending() { + error!("Wallet is locked"); + return object!{ + "error" => "Wallet is locked" + }; + } + // Clone address so it can be moved into the closure let address = addr.clone(); let wallet = self.wallet.read().unwrap(); @@ -332,6 +339,22 @@ impl LightClient { } pub fn do_save(&self) -> Result<(), String> { + + // If the wallet is encrypted but unlocked, lock it again. + { + let mut wallet = self.wallet.write().unwrap(); + if wallet.is_encrypted() && wallet.is_unlocked_for_spending() { + match wallet.lock() { + Ok(_) => {}, + Err(e) => { + let err = format!("ERR: {}", e); + error!("{}", err); + return Err(e.to_string()); + } + } + } + } + let mut file_buffer = BufWriter::with_capacity( 1_000_000, // 1 MB write buffer File::create(self.config.get_wallet_path()).unwrap()); @@ -369,6 +392,13 @@ impl LightClient { } pub fn do_seed_phrase(&self) -> JsonValue { + if !self.wallet.read().unwrap().is_unlocked_for_spending() { + error!("Wallet is locked"); + return object!{ + "error" => "Wallet is locked" + }; + } + let wallet = self.wallet.read().unwrap(); object!{ "seed" => wallet.get_seed_phrase(), @@ -549,6 +579,13 @@ impl LightClient { /// Create a new address, deriving it from the seed. pub fn do_new_address(&self, addr_type: &str) -> JsonValue { + if !self.wallet.read().unwrap().is_unlocked_for_spending() { + error!("Wallet is locked"); + return object!{ + "error" => "Wallet is locked" + }; + } + let wallet = self.wallet.write().unwrap(); let new_address = match addr_type { @@ -784,6 +821,11 @@ impl LightClient { } pub fn do_send(&self, addrs: Vec<(&str, u64, Option)>) -> Result { + if !self.wallet.read().unwrap().is_unlocked_for_spending() { + error!("Wallet is locked"); + return Err("Wallet is locked".to_string()); + } + info!("Creating transaction"); let rawtx = self.wallet.write().unwrap().send_to_address( diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 1c17c65..5fa0d67 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -91,7 +91,14 @@ impl ToBase58Check for [u8] { } pub struct LightWallet { - locked: bool, // Is the wallet's spending keys locked? + // Is the wallet encrypted? If it is, then when writing to disk, the seed is always encrypted + // and the individual spending keys are not written + encrypted: bool, + + // In memory only (i.e, this field is not written to disk). Is the wallet unlocked and are + // the spending keys present to allow spending from this wallet? + unlocked: bool, + enc_seed: [u8; 48], // If locked, this contains the encrypted seed nonce: Vec, // Nonce used to encrypt the wallet. @@ -184,7 +191,8 @@ impl LightWallet { = LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0); Ok(LightWallet { - locked: false, + encrypted: false, + unlocked: true, enc_seed: [0u8; 48], nonce: vec![], seed: seed_bytes, @@ -210,7 +218,7 @@ impl LightWallet { info!("Reading wallet version {}", version); - let locked = if version >= 4 { + let encrypted = if version >= 4 { reader.read_u8()? > 0 } else { false @@ -281,7 +289,8 @@ impl LightWallet { let birthday = reader.read_u64::()?; Ok(LightWallet{ - locked: locked, + encrypted: encrypted, + unlocked: !encrypted, // When reading from disk, if wallet is encrypted, it starts off locked. enc_seed: enc_seed, nonce: nonce, seed: seed_bytes, @@ -298,11 +307,16 @@ impl LightWallet { } pub fn write(&self, mut writer: W) -> io::Result<()> { + if self.encrypted && self.unlocked { + return Err(Error::new(ErrorKind::InvalidInput, + format!("Cannot write while wallet is unlocked while encrypted."))); + } + // Write the version writer.write_u64::(LightWallet::serialized_version())?; // Write if it is locked - writer.write_u8(if self.locked {1} else {0})?; + writer.write_u8(if self.encrypted {1} else {0})?; // Write the encrypted seed bytes writer.write_all(&self.enc_seed)?; @@ -400,6 +414,10 @@ impl LightWallet { /// at the next position and add it to the wallet. /// NOTE: This does NOT rescan pub fn add_zaddr(&self) -> String { + if !self.unlocked { + return "".to_string(); + } + let pos = self.extsks.read().unwrap().len() as u32; let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), ""); @@ -418,6 +436,10 @@ impl LightWallet { /// at the next position. /// NOTE: This is not rescan the wallet pub fn add_taddr(&self) -> String { + if !self.unlocked { + return "".to_string(); + } + let pos = self.tkeys.read().unwrap().len() as u32; let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), ""); @@ -562,16 +584,20 @@ impl LightWallet { } pub fn get_seed_phrase(&self) -> String { + if !self.unlocked { + return "".to_string(); + } + Mnemonic::from_entropy(&self.seed, Language::English, ).unwrap().phrase().to_string() } - pub fn lock(&mut self, passwd: String) -> io::Result<()> { + pub fn encrypt(&mut self, passwd: String) -> io::Result<()> { use sodiumoxide::crypto::secretbox; - if self.locked { - return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is already locked")); + if self.encrypted && !self.unlocked { + return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is already encrypted and locked")); } // Get the doublesha256 of the password, which is the right length @@ -584,20 +610,32 @@ impl LightWallet { self.nonce = vec![]; self.nonce.extend_from_slice(nonce.as_ref()); + self.encrypted = true; + self.lock()?; + + Ok(()) + } + + pub fn lock(&mut self) -> io::Result<()> { // Empty the seed and the secret keys self.seed.copy_from_slice(&[0u8; 32]); self.tkeys = Arc::new(RwLock::new(vec![])); self.extsks = Arc::new(RwLock::new(vec![])); - self.locked = true; + self.unlocked = false; + Ok(()) } pub fn unlock(&mut self, passwd: String) -> io::Result<()> { - use sodiumoxide::crypto::secretbox; + use sodiumoxide::crypto::secretbox; - if !self.locked { - return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is not locked")); + if !self.encrypted { + return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is not encrypted")); + } + + if self.encrypted && self.unlocked { + return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is already unlocked")); } // Get the doublesha256 of the password, which is the right length @@ -650,18 +688,45 @@ impl LightWallet { tkeys.push(sk); } - // Everything checks out, so we'll update our wallet with the unlocked values + // Everything checks out, so we'll update our wallet with the decrypted values self.extsks = Arc::new(RwLock::new(extsks)); self.tkeys = Arc::new(RwLock::new(tkeys)); self.seed.copy_from_slice(&seed); + + self.encrypted = true; + self.unlocked = true; + + Ok(()) + } + + // Removing encryption means unlocking it and setting the self.encrypted = false, + // permanantly removing the encryption + pub fn remove_encryption(&mut self, passwd: String) -> io::Result<()> { + if !self.encrypted { + return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is not encrypted")); + } + + // Unlock the wallet if it's locked + if !self.unlocked { + self.unlock(passwd)?; + } + // Permanantly remove the encryption + self.encrypted = false; self.nonce = vec![]; self.enc_seed.copy_from_slice(&[0u8; 48]); - self.locked = false; Ok(()) } + pub fn is_encrypted(&self) -> bool { + return self.encrypted; + } + + pub fn is_unlocked_for_spending(&self) -> bool { + return self.unlocked; + } + pub fn zbalance(&self, addr: Option) -> u64 { self.txs.read().unwrap() .values() @@ -1245,6 +1310,10 @@ impl LightWallet { output_params: &[u8], tos: Vec<(&str, u64, Option)> ) -> Result, String> { + if !self.unlocked { + return Err("Cannot spend while wallet is locked".to_string()); + } + let start_time = now(); let total_value = tos.iter().map(|to| to.1).sum::(); @@ -3083,10 +3152,10 @@ pub mod tests { let seed = wallet.seed; - wallet.lock("somepassword".to_string()).unwrap(); + wallet.encrypt("somepassword".to_string()).unwrap(); - // Locking a locked wallet should fail - assert!(wallet.lock("somepassword".to_string()).is_err()); + // Encrypting an already encrypted wallet should fail + assert!(wallet.encrypt("somepassword".to_string()).is_err()); // Serialize a locked wallet let mut serialized_data = vec![]; @@ -3120,6 +3189,12 @@ pub mod tests { // Unlocking an already unlocked wallet should fail assert!(wallet.unlock("somepassword".to_string()).is_err()); + // Trying to serialize a encrypted but unlocked wallet should fail + assert!(wallet.write(&mut vec![]).is_err()); + + // ...but if we lock it again, it should serialize + wallet.lock().unwrap(); + wallet.write(&mut vec![]).expect("Serialize wallet"); // Try from a deserialized, locked wallet let mut wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap(); From e4c6180b42f2fc8402205f6926cfd551481a9ccd Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sun, 20 Oct 2019 11:56:45 -0700 Subject: [PATCH 19/24] Split wallet creation --- lib/src/lightclient.rs | 66 ++++++++++++++++++++++++++---------------- src/main.rs | 5 +++- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index fa51cad..acc6cf3 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -87,6 +87,10 @@ impl LightClientConfig { wallet_location.into_boxed_path() } + pub fn wallet_exists(&self) -> bool { + return self.get_wallet_path().exists() + } + pub fn get_log_path(&self) -> Box { let mut log_path = self.get_zcash_data_path().into_path_buf(); log_path.push(LOGFILE_NAME); @@ -201,42 +205,54 @@ impl LightClient { }; } - pub fn new(seed_phrase: Option, config: &LightClientConfig, latest_block: u64) -> io::Result { - let mut lc = if config.get_wallet_path().exists() { - // Make sure that if a wallet exists, there is no seed phrase being attempted - if !seed_phrase.is_none() { - return Err(Error::new(ErrorKind::AlreadyExists, + fn read_sapling_params(&mut self) { + // Read Sapling Params + self.sapling_output.extend_from_slice(SaplingParams::get("sapling-output.params").unwrap().as_ref()); + self.sapling_spend.extend_from_slice(SaplingParams::get("sapling-spend.params").unwrap().as_ref()); + + } + + pub fn new_from_phrase(seed_phrase: String, config: &LightClientConfig, latest_block: u64) -> io::Result { + if config.get_wallet_path().exists() { + return Err(Error::new(ErrorKind::AlreadyExists, "Cannot create a new wallet from seed, because a wallet already exists")); - } + } - let mut file_buffer = BufReader::new(File::open(config.get_wallet_path())?); - - let wallet = LightWallet::read(&mut file_buffer, config)?; - LightClient { - wallet : Arc::new(RwLock::new(wallet)), - config : config.clone(), - sapling_output : vec![], - sapling_spend : vec![] - } - } else { - let l = LightClient { - wallet : Arc::new(RwLock::new(LightWallet::new(seed_phrase, config, latest_block)?)), + let mut l = LightClient { + wallet : Arc::new(RwLock::new(LightWallet::new(Some(seed_phrase), config, latest_block)?)), config : config.clone(), sapling_output : vec![], sapling_spend : vec![] }; - l.set_wallet_initial_state(); + l.set_wallet_initial_state(); + l.read_sapling_params(); + + info!("Created new wallet!"); + info!("Created LightClient to {}", &config.server); + + Ok(l) + } + + pub fn read_from_disk(config: &LightClientConfig) -> io::Result { + if !config.get_wallet_path().exists() { + return Err(Error::new(ErrorKind::AlreadyExists, + format!("Cannot read wallet. No file at {}", config.get_wallet_path().display()))); + } - l + let mut file_buffer = BufReader::new(File::open(config.get_wallet_path())?); + + let wallet = LightWallet::read(&mut file_buffer, config)?; + let mut lc = LightClient { + wallet : Arc::new(RwLock::new(wallet)), + config : config.clone(), + sapling_output : vec![], + sapling_spend : vec![] }; - info!("Read wallet with birthday {}", lc.wallet.read().unwrap().get_first_tx_block()); - - // Read Sapling Params - lc.sapling_output.extend_from_slice(SaplingParams::get("sapling-output.params").unwrap().as_ref()); - lc.sapling_spend.extend_from_slice(SaplingParams::get("sapling-spend.params").unwrap().as_ref()); + lc.read_sapling_params(); + info!("Read wallet with birthday {}", lc.wallet.read().unwrap().get_first_tx_block()); info!("Created LightClient to {}", &config.server); Ok(lc) diff --git a/src/main.rs b/src/main.rs index d9b0ecc..780a08c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -160,7 +160,10 @@ fn startup(server: http::Uri, dangerous: bool, seed: Option, first_sync: std::io::Error::new(ErrorKind::Other, e) })?; - let lightclient = Arc::new(LightClient::new(seed, &config, latest_block_height)?); + let lightclient = match seed { + Some(phrase) => Arc::new(LightClient::new_from_phrase(phrase, &config, latest_block_height)?), + None => Arc::new(LightClient::read_from_disk(&config)?) + }; // Print startup Messages info!(""); // Blank line From 4b21f7c5304d00834cab9f20d11a7a1669e40d1c Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sun, 20 Oct 2019 13:13:43 -0700 Subject: [PATCH 20/24] Add bug description --- bip39bug.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 bip39bug.md diff --git a/bip39bug.md b/bip39bug.md new file mode 100644 index 0000000..d95b299 --- /dev/null +++ b/bip39bug.md @@ -0,0 +1,13 @@ +## Zecwallet-cli BIP39 derivation bug + +In v1.0 of zecwallet-cli, there was a bug that incorrectly derived HD wallet keys after the first key. That is, the first key, address was correct, but subsequent ones were not. + +The issue was that the 32-byte seed was directly being used to derive then subsequent addresses instead of the 64-byte pkdf2(seed). The issue affected both t and z addresses. + +Note that no funds are at risk. The issue is that, if in the future, you import the seed into a different wallet, you might not see all your addresses in the new wallet, so it's better to fix it now. + +## Fix +If you start a wallet that has this bug, you'll be notified. +The bug can be automatically fixed by the wallet by running `fixbip39bug` + +If you have any funds in the incorrect addresses, they'll be sent to yourself, and the correct addresses re-derived. \ No newline at end of file From 085b8492ba393eeaf2ab011a7df7a82e56c33138 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sun, 20 Oct 2019 13:14:34 -0700 Subject: [PATCH 21/24] update copy --- bip39bug.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bip39bug.md b/bip39bug.md index d95b299..bb62319 100644 --- a/bip39bug.md +++ b/bip39bug.md @@ -8,6 +8,6 @@ Note that no funds are at risk. The issue is that, if in the future, you import ## Fix If you start a wallet that has this bug, you'll be notified. -The bug can be automatically fixed by the wallet by running `fixbip39bug` +The bug can be automatically fixed by the wallet by running the `fixbip39bug` command. Just start `zecwallet-cli` and type `fixbip39bug`. If you have any funds in the incorrect addresses, they'll be sent to yourself, and the correct addresses re-derived. \ No newline at end of file From c1dd259caf7cff8690d78d2fedc3f06c5c7630f7 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sun, 20 Oct 2019 13:40:54 -0700 Subject: [PATCH 22/24] Add bug warning --- lib/src/lightclient.rs | 18 ++++++++++++++++++ lib/src/lightwallet/bugs.rs | 1 + 2 files changed, 19 insertions(+) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index acc6cf3..fcaf7a5 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -42,6 +42,18 @@ pub struct LightClientConfig { impl LightClientConfig { + // Create an unconnected (to any server) config to test for local wallet etc... + pub fn create_unconnected(chain_name: String) -> LightClientConfig { + LightClientConfig { + server : http::Uri::default(), + chain_name : chain_name, + sapling_activation_height : 0, + consensus_branch_id : "".to_string(), + anchor_offset : ANCHOR_OFFSET, + no_cert_verification : false, + } + } + pub fn create(server: http::Uri, dangerous: bool) -> io::Result<(LightClientConfig, u64)> { // Do a getinfo first, before opening the wallet let info = grpcconnector::get_info(server.clone(), dangerous) @@ -255,6 +267,12 @@ impl LightClient { info!("Read wallet with birthday {}", lc.wallet.read().unwrap().get_first_tx_block()); info!("Created LightClient to {}", &config.server); + if crate::lightwallet::bugs::BugBip39Derivation::has_bug(&lc) { + let m = format!("WARNING!!!\nYour wallet has a bip39derivation bug that's showing incorrect addresses.\nPlease run 'fixbip39bug' to automatically fix the address derivation in your wallet!\nPlease see: https://github.com/adityapk00/zecwallet-light-cli/blob/master/bip39bug.md"); + info!("{}", m); + println!("{}", m); + } + Ok(lc) } diff --git a/lib/src/lightwallet/bugs.rs b/lib/src/lightwallet/bugs.rs index 60f237f..48eeba7 100644 --- a/lib/src/lightwallet/bugs.rs +++ b/lib/src/lightwallet/bugs.rs @@ -70,6 +70,7 @@ impl BugBip39Derivation { // Tranfer money // 1. The desination is z address #0 + println!("Sending funds to ourself."); let zaddr = client.do_address()["z_addresses"][0].as_str().unwrap().to_string(); let balance_json = client.do_balance(); let fee: u64 = DEFAULT_FEE.try_into().unwrap(); From 9ed9e47ff94a97645a964be3141531d90cadfa9b Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sun, 20 Oct 2019 20:31:45 -0700 Subject: [PATCH 23/24] Add lightclient encryption test --- lib/src/commands.rs | 16 ++++++-- lib/src/lightclient.rs | 86 ++++++++++++++++++++++++++++++++---------- 2 files changed, 79 insertions(+), 23 deletions(-) diff --git a/lib/src/commands.rs b/lib/src/commands.rs index 98dbd4d..fd6b2ca 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -196,8 +196,10 @@ impl Command for ExportCommand { } let address = if args.is_empty() { None } else { Some(args[0].to_string()) }; - - format!("{}", lightclient.do_export(address).pretty(2)) + match lightclient.do_export(address) { + Ok(j) => j, + Err(e) => object!{ "error" => e } + }.pretty(2) } } @@ -462,7 +464,10 @@ impl Command for SeedCommand { } fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { - format!("{}", lightclient.do_seed_phrase().pretty(2)) + match lightclient.do_seed_phrase() { + Ok(j) => j, + Err(e) => object!{ "error" => e } + }.pretty(2) } } @@ -537,7 +542,10 @@ impl Command for NewAddressCommand { return format!("No address type specified\n{}", self.help()); } - format!("{}", lightclient.do_new_address(args[0]).pretty(2)) + match lightclient.do_new_address(args[0]) { + Ok(j) => j, + Err(e) => object!{ "error" => e } + }.pretty(2) } } diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index fcaf7a5..846ca02 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -224,6 +224,26 @@ impl LightClient { } + /// Method to create a test-only version of the LightClient + #[allow(dead_code)] + fn unconnected(seed_phrase: String) -> io::Result { + let config = LightClientConfig::create_unconnected("test".to_string()); + let mut l = LightClient { + wallet : Arc::new(RwLock::new(LightWallet::new(Some(seed_phrase), &config, 0)?)), + config : config.clone(), + sapling_output : vec![], + sapling_spend : vec![] + }; + + l.set_wallet_initial_state(); + l.read_sapling_params(); + + info!("Created new wallet!"); + info!("Created LightClient to {}", &config.server); + + Ok(l) + } + pub fn new_from_phrase(seed_phrase: String, config: &LightClientConfig, latest_block: u64) -> io::Result { if config.get_wallet_path().exists() { return Err(Error::new(ErrorKind::AlreadyExists, @@ -281,12 +301,10 @@ impl LightClient { } // Export private keys - pub fn do_export(&self, addr: Option) -> JsonValue { + pub fn do_export(&self, addr: Option) -> Result { if !self.wallet.read().unwrap().is_unlocked_for_spending() { error!("Wallet is locked"); - return object!{ - "error" => "Wallet is locked" - }; + return Err("Wallet is locked"); } // Clone address so it can be moved into the closure @@ -319,11 +337,12 @@ impl LightClient { all_keys.extend_from_slice(&z_keys); all_keys.extend_from_slice(&t_keys); - all_keys.into() + Ok(all_keys.into()) } pub fn do_address(&self) -> JsonValue { let wallet = self.wallet.read().unwrap(); + // Collect z addresses let z_addresses = wallet.zaddress.read().unwrap().iter().map( |ad| { encode_payment_address(self.config.hrp_sapling_address(), &ad) @@ -425,19 +444,17 @@ impl LightClient { } } - pub fn do_seed_phrase(&self) -> JsonValue { + pub fn do_seed_phrase(&self) -> Result { if !self.wallet.read().unwrap().is_unlocked_for_spending() { error!("Wallet is locked"); - return object!{ - "error" => "Wallet is locked" - }; + return Err("Wallet is locked"); } let wallet = self.wallet.read().unwrap(); - object!{ + Ok(object!{ "seed" => wallet.get_seed_phrase(), "birthday" => wallet.get_birthday() - } + }) } // Return a list of all notes, spent and unspent @@ -612,12 +629,10 @@ impl LightClient { } /// Create a new address, deriving it from the seed. - pub fn do_new_address(&self, addr_type: &str) -> JsonValue { + pub fn do_new_address(&self, addr_type: &str) -> Result { if !self.wallet.read().unwrap().is_unlocked_for_spending() { error!("Wallet is locked"); - return object!{ - "error" => "Wallet is locked" - }; + return Err("Wallet is locked".to_string()); } let wallet = self.wallet.write().unwrap(); @@ -628,13 +643,11 @@ impl LightClient { _ => { let e = format!("Unrecognized address type: {}", addr_type); error!("{}", e); - return object!{ - "error" => e - }; + return Err(e); } }; - array![new_address] + Ok(array![new_address]) } pub fn do_rescan(&self) -> String { @@ -874,3 +887,38 @@ impl LightClient { } } } + + +pub mod tests { + use lazy_static::lazy_static; + //use super::LightClient; + + lazy_static!{ + static ref TEST_SEED: String = "youth strong sweet gorilla hammer unhappy congress stamp left stereo riot salute road tag clean toilet artefact fork certain leopard entire civil degree wonder".to_string(); + } + + #[test] + pub fn test_encrypt_decrypt() { + let lc = super::LightClient::unconnected(TEST_SEED.to_string()).unwrap(); + + assert!(!lc.do_export(None).is_err()); + assert!(!lc.do_new_address("z").is_err()); + assert!(!lc.do_new_address("t").is_err()); + assert_eq!(lc.do_seed_phrase().unwrap()["seed"], TEST_SEED.to_string()); + + // Encrypt and Lock the wallet + lc.wallet.write().unwrap().encrypt("password".to_string()).unwrap(); + assert!(lc.do_export(None).is_err()); + assert!(lc.do_seed_phrase().is_err()); + assert!(lc.do_new_address("t").is_err()); + assert!(lc.do_new_address("z").is_err()); + assert!(lc.do_send(vec![("z", 0, None)]).is_err()); + + // Do a unlock, and make sure it all works now + lc.wallet.write().unwrap().unlock("password".to_string()).unwrap(); + assert!(!lc.do_export(None).is_err()); + assert!(!lc.do_seed_phrase().is_err()); + assert!(!lc.do_new_address("t").is_err()); + assert!(!lc.do_new_address("z").is_err()); + } +} \ No newline at end of file From be53b8087fcaf40bbce6662ceaf22b05ad91f78c Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sun, 20 Oct 2019 20:46:37 -0700 Subject: [PATCH 24/24] LC address tests --- lib/src/lightclient.rs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 846ca02..877bebc 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -391,8 +391,7 @@ impl LightClient { } } - pub fn do_save(&self) -> Result<(), String> { - + pub fn do_save(&self) -> Result<(), String> { // If the wallet is encrypted but unlocked, lock it again. { let mut wallet = self.wallet.write().unwrap(); @@ -921,4 +920,27 @@ pub mod tests { assert!(!lc.do_new_address("t").is_err()); assert!(!lc.do_new_address("z").is_err()); } + + #[test] + pub fn test_addresses() { + let lc = super::LightClient::unconnected(TEST_SEED.to_string()).unwrap(); + + // Add new z and t addresses + + let taddr1 = lc.do_new_address("t").unwrap()[0].as_str().unwrap().to_string(); + let taddr2 = lc.do_new_address("t").unwrap()[0].as_str().unwrap().to_string(); + let zaddr1 = lc.do_new_address("z").unwrap()[0].as_str().unwrap().to_string(); + let zaddr2 = lc.do_new_address("z").unwrap()[0].as_str().unwrap().to_string(); + + let addresses = lc.do_address(); + assert_eq!(addresses["z_addresses"].len(), 3); + assert_eq!(addresses["z_addresses"][1], zaddr1); + assert_eq!(addresses["z_addresses"][2], zaddr2); + + assert_eq!(addresses["t_addresses"].len(), 3); + assert_eq!(addresses["t_addresses"][1], taddr1); + assert_eq!(addresses["t_addresses"][2], taddr2); + } + + } \ No newline at end of file