Browse Source

merged new updates

checkpoints
DenioD 5 years ago
parent
commit
8c9523ad90
  1. 1
      lib/Cargo.toml
  2. 264
      lib/src/commands.rs
  3. 8
      lib/src/grpcconnector.rs
  4. 1
      lib/src/lib.rs
  5. 450
      lib/src/lightclient.rs
  6. 512
      lib/src/lightwallet.rs
  7. 126
      lib/src/lightwallet/bugs.rs
  8. 24
      lib/src/lightwallet/data.rs
  9. 20
      lib/src/lightwallet/startup_helpers.rs
  10. 20
      lib/src/startup_helpers.rs
  11. 18
      src/main.rs

1
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/DenioD/librustzcash.git"

264
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 {
@ -209,6 +327,8 @@ impl Command for SendCommand {
h.push("Send HUSH to a given address");
h.push("Usage:");
h.push("send <address> <amount in puposhis> \"optional_memo\"");
h.push("OR");
h.push("send '[{'address': <address>, 'amount': <amount in zatoshis>, 'memo': <optional memo>}, ...]'");
h.push("");
h.push("Example:");
h.push("send ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d 200000 \"Hello from the command line\"");
@ -222,25 +342,70 @@ 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::<u64>() {
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::<Result<Vec<(&str, u64, Option<String>)>, String>>();
let send_args = match maybe_send_args {
Ok(a) => a,
Err(s) => { return format!("Error: {}\n{}", s, self.help()); }
};
lightclient.do_sync(true);
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::<u64>() {
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_send(args[0], value, memo)
lightclient.do_sync(true);
match lightclient.do_send(vec!((args[0], value, memo))) {
Ok(txid) => { object!{ "txid" => txid } },
Err(e) => { object!{ "error" => e } }
}.pretty(2)
} else {
self.help()
}
}
}
@ -263,7 +428,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)
}
}
}
}
@ -403,6 +580,28 @@ 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;
BugBip39Derivation::fix_bug(lightclient)
}
}
struct QuitCommand {}
impl Command for QuitCommand {
@ -421,28 +620,35 @@ 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
}
}
}
pub fn get_commands() -> Box<HashMap<String, Box<dyn Command>>> {
let mut map: HashMap<String, Box<dyn Command>> = 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("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)
}

8
lib/src/grpcconnector.rs

@ -1,4 +1,3 @@
use log::{error};
use std::sync::{Arc};
@ -280,7 +279,12 @@ 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();
}
Ok(txid)
} else {
Err(format!("Error: {:?}", sendresponse))
}

1
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;

450
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::{
@ -23,8 +26,8 @@ use crate::SaplingParams;
use crate::ANCHOR_OFFSET;
pub const DEFAULT_SERVER: &str = "https://";
pub const WALLET_NAME: &str = "silentdragonlite-cli.dat";
pub const LOGFILE_NAME: &str = "silentdragonlite-cli.debug.log";
pub const WALLET_NAME: &str = "silentdragonlite-cli-wallet.dat";
pub const LOGFILE_NAME: &str = "silentdragonlite-cli-wallet.debug.log";
#[derive(Clone, Debug)]
@ -39,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)
@ -64,7 +79,7 @@ impl LightClientConfig {
zcash_data_location.push("HUSH3");
} else {
zcash_data_location = dirs::home_dir().expect("Couldn't determine home directory!");
zcash_data_location.push(".komodo/HUSH3/SilentDragonLite/");
zcash_data_location.push(".komodo/HUSH3/");
};
match &self.chain_name[..] {
@ -84,6 +99,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<Path> {
let mut log_path = self.get_zcash_data_path().into_path_buf();
log_path.push(LOGFILE_NAME);
@ -99,7 +118,7 @@ impl LightClientConfig {
)),
"main" => Some((105944,
"0000000313b0ec7c5a1e9b997ce44a7763b56c5505526c36634a004ed52d7787",
""
""
)),
_ => None
}
@ -150,7 +169,6 @@ impl LightClientConfig {
match &self.chain_name[..] {
"main" => mainnet::B58_PUBKEY_ADDRESS_PREFIX,
c => panic!("Unknown chain {}", c)
}
}
@ -159,8 +177,7 @@ impl LightClientConfig {
pub fn base58_script_address(&self) -> [u8; 1] {
match &self.chain_name[..] {
"main" => mainnet::B58_SCRIPT_ADDRESS_PREFIX,
c => panic!("Unknown chain {}", c)
}
}
@ -176,7 +193,7 @@ impl LightClientConfig {
}
pub struct LightClient {
pub wallet : Arc<LightWallet>,
pub wallet : Arc<RwLock<LightWallet>>,
pub config : LightClientConfig,
@ -193,63 +210,88 @@ 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,
};
}
pub fn new(seed_phrase: Option<String>, config: &LightClientConfig, latest_block: u64) -> io::Result<Self> {
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<Self> {
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(wallet),
config : config.clone(),
sapling_output : vec![],
sapling_spend : vec![]
}
} else {
let l = LightClient {
wallet : Arc::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)
}
l
pub fn read_from_disk(config: &LightClientConfig) -> io::Result<Self> {
if !config.get_wallet_path().exists() {
return Err(Error::new(ErrorKind::AlreadyExists,
format!("Cannot read wallet. No file at {}", config.get_wallet_path().display())));
}
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.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);
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)
}
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<String>) -> 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();
// 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!{
@ -262,7 +304,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!{
@ -279,15 +321,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::<Vec<String>>();
// Collect t addresses
let t_addresses = self.wallet.tkeys.read().unwrap().iter().map( |sk| {
self.wallet.address_from_sk(&sk)
}).collect::<Vec<String>>();
let t_addresses = wallet.taddresses.read().unwrap().iter().map( |a| a.clone() )
.collect::<Vec<String>>();
object!{
"z_addresses" => z_addresses,
@ -296,47 +338,67 @@ 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::<Vec<JsonValue>>();
// 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::<Vec<JsonValue>>();
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,
}
}
pub fn do_save(&self) -> 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();
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());
self.wallet.write(&mut file_buffer).unwrap();
info!("Saved wallet");
format!("Saved Wallet")
match self.wallet.write().unwrap().write(&mut file_buffer) {
Ok(_) => Ok(()),
Err(e) => {
let err = format!("ERR: {}", e);
error!("{}", err);
Err(e.to_string())
}
}
}
pub fn get_server_uri(&self) -> http::Uri {
@ -362,9 +424,17 @@ 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" => self.wallet.get_seed_phrase(),
"birthday" => self.wallet.get_birthday()
"seed" => wallet.get_seed_phrase(),
"birthday" => wallet.get_birthday()
}
}
@ -374,108 +444,95 @@ impl LightClient {
let mut spent_notes : Vec<JsonValue> = vec![];
let mut pending_notes: Vec<JsonValue> = 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,
"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);
}
});
});
}
// 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::<Vec<JsonValue>>();
// 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)),
}
)
.collect::<Vec<JsonValue>>();;
let mut unspent_utxos: Vec<JsonValue> = vec![];
let mut spent_utxos : Vec<JsonValue> = vec![];
let mut pending_utxos: Vec<JsonValue> = vec![];
{
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 {
pending_utxos.push(utxo);
}
});
}
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::<Vec<JsonValue>>()
}).collect::<Vec<JsonValue>>()
);
res["spent_utxos"] = JsonValue::Array(spent_utxos);
}
res
}
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<JsonValue> = vec![];
@ -501,6 +558,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
@ -515,9 +573,10 @@ 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),
"address" => LightWallet::note_address(self.config.hrp_sapling_address(), nd),
"memo" => LightWallet::memo_str(&nd.memo),
})
);
@ -528,6 +587,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::<Vec<String>>().join(","),
@ -551,9 +611,18 @@ 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 {
"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);
@ -569,7 +638,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();
@ -587,7 +656,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));
@ -626,6 +695,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();
@ -640,7 +713,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();
@ -651,8 +726,18 @@ impl LightClient {
return;
}
match local_light_wallet.scan_block(encoded_block) {
let block: Result<zcash_client_backend::proto::compact_formats::CompactBlock, _>
= parse_from_bytes(encoded_block);
match block {
Ok(b) => {
block_times_inner.write().unwrap().insert(b.height, b.time);
},
Err(_) => {}
}
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::<Vec<_>>()[..]);
},
Err(invalid_height) => {
@ -667,7 +752,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);
}
@ -693,18 +778,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
wallet.scan_full_tx(&tx, height as i32);
{
// 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::<Vec<String>>();
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;
@ -712,8 +807,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
}
@ -727,10 +823,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::<Vec<(TxId, i32)>>();
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::<Vec<(TxId, i32)>>();
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()[..]);
@ -742,7 +838,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);
@ -750,27 +845,30 @@ 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.read().unwrap().scan_full_tx(&tx, height, 0);
});
};
responses.join("\n")
}
pub fn do_send(&self, addr: &str, value: u64, memo: Option<String>) -> String {
pub fn do_send(&self, addrs: Vec<(&str, u64, Option<String>)>) -> Result<String, String> {
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.send_to_address(
u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), // Blossom ID
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,
vec![(&addr, value, memo)]
addrs
);
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!("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))
}
}
}

512
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};
@ -90,17 +91,30 @@ impl ToBase58Check for [u8] {
}
pub struct LightWallet {
seed: [u8; 32], // Seed phrase for this wallet.
// 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,
// 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
// 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<u8>, // Nonce used to encrypt the wallet.
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)
extsks: Arc<RwLock<Vec<ExtendedSpendingKey>>>,
extfvks: Arc<RwLock<Vec<ExtendedFullViewingKey>>>,
pub address: Arc<RwLock<Vec<PaymentAddress<Bls12>>>>,
pub zaddress: Arc<RwLock<Vec<PaymentAddress<Bls12>>>>,
// Transparent keys. TODO: Make it not pubic
pub tkeys: Arc<RwLock<Vec<secp256k1::SecretKey>>>,
// Transparent keys. If the wallet is locked, then the secret keys will be encrypted,
// but the addresses will be present.
tkeys: Arc<RwLock<Vec<secp256k1::SecretKey>>>,
pub taddresses: Arc<RwLock<Vec<String>>>,
blocks: Arc<RwLock<Vec<BlockData>>>,
pub txs: Arc<RwLock<HashMap<TxId, WalletTx>>>,
@ -115,10 +129,12 @@ 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 {
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()
@ -130,10 +146,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<Bls12>) {
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()),
@ -163,8 +181,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
@ -172,23 +191,50 @@ impl LightWallet {
= LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0);
Ok(LightWallet {
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,
encrypted: false,
unlocked: true,
enc_seed: [0u8; 48],
nonce: vec![],
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,
})
}
pub fn read<R: Read>(mut reader: R, config: &LightClientConfig) -> io::Result<Self> {
let version = reader.read_u64::<LittleEndian>()?;
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 encrypted = if version >= 4 {
reader.read_u8()? > 0
} else {
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)?;
@ -196,9 +242,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::<Vec<ExtendedFullViewingKey>>();
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::<Vec<ExtendedFullViewingKey>>()
};
// Calculate the addresses
let addresses = extfvks.iter().map( |fvk| fvk.default_address().unwrap().1 )
@ -210,6 +261,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| {
@ -230,22 +289,41 @@ impl LightWallet {
let birthday = reader.read_u64::<LittleEndian>()?;
Ok(LightWallet{
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(),
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,
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,
})
}
pub fn write<W: 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::<LittleEndian>(LightWallet::serialized_version())?;
// Write if it is locked
writer.write_u8(if self.encrypted {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)?;
@ -257,11 +335,21 @@ impl LightWallet {
|w, sk| sk.write(w)
)?;
// Write the transparent private key
// Write the FVKs
Vector::write(&mut writer, &self.extfvks.read().unwrap(),
|w, fvk| fvk.write(w)
)?;
// 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
@ -279,9 +367,9 @@ impl LightWallet {
Ok(())
}
pub fn note_address(&self, note: &SaplingNoteData) -> Option<String> {
pub fn note_address(hrp: &str, note: &SaplingNoteData) -> Option<String> {
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
}
}
@ -317,7 +405,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::<Vec<(String, String)>>()
}
@ -325,14 +414,20 @@ 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(), "");
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);
self.extfvks.write().unwrap().push(extfvk);
self.address.write().unwrap().push(address);
self.zaddress.write().unwrap().push(address);
zaddr
}
@ -341,12 +436,20 @@ 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 sk = LightWallet::get_taddr_from_bip39seed(&self.config, &self.seed, pos);
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), "");
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);
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.
@ -453,7 +556,7 @@ impl LightWallet {
}
}
pub fn address_from_sk(&self, sk: &secp256k1::SecretKey) -> String {
pub fn address_from_prefix_sk(prefix: &[u8; 1], sk: &secp256k1::SecretKey) -> String {
let secp = secp256k1::Secp256k1::new();
let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk);
@ -461,7 +564,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<TransparentAddress>) -> Option<String> {
@ -477,11 +584,149 @@ 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 encrypt(&mut self, passwd: String) -> io::Result<()> {
use sodiumoxide::crypto::secretbox;
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
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());
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.unlocked = false;
Ok(())
}
pub fn unlock(&mut self, passwd: String) -> io::Result<()> {
use sodiumoxide::crypto::secretbox;
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
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 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]);
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<String>) -> u64 {
self.txs.read().unwrap()
.values()
@ -560,12 +805,12 @@ impl LightWallet {
.sum::<u64>()
}
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 +845,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 +884,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 +906,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);
}
},
_ => {}
@ -676,8 +921,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::<HashSet<String>>();
for vout in tx.vout.iter() {
@ -745,7 +990,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::<HashSet<String>>();
@ -1013,7 +1258,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();
@ -1065,6 +1310,10 @@ impl LightWallet {
output_params: &[u8],
tos: Vec<(&str, u64, Option<String>)>
) -> Result<Box<[u8]>, 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::<u64>();
@ -1175,7 +1424,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);
@ -1669,7 +1918,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 +1943,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 +1990,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 +2015,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 +2064,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 +2077,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
@ -1847,7 +2096,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]);
@ -1916,7 +2165,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]));
@ -2023,7 +2272,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 +2312,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 +2365,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 +2436,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 +2464,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 +2526,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 +2576,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();
@ -2336,7 +2585,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));
}
}
@ -2366,7 +2615,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 +2673,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 +2717,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 +2754,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 +2810,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 +2882,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 +3060,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.
{
@ -2868,9 +3117,15 @@ pub mod tests {
// Test the addresses against https://iancoleman.io/bip39/
let (taddr, pk) = &wallet.get_t_secret_keys()[0];
assert_eq!(taddr, "RVog7rQu2Zo2iAQCjbZGXsiQm7SYr9bcaq");
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");
@ -2878,9 +3133,112 @@ 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.encrypt("somepassword".to_string()).unwrap();
// Encrypting an already encrypted wallet should fail
assert!(wallet.encrypt("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());
// 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();
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]
#[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 = 50000;
const AMOUNT: u64 = 500000;
let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT);
let prev_hash = add_blocks(&wallet, 2, 1, block_hash).unwrap();

126
lib/src/lightwallet/bugs.rs

@ -0,0 +1,126 @@
///
/// 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;
use json::object;
use bip39::{Mnemonic, Language};
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();
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
}
/// 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;
if !BugBip39Derivation::has_bug(client) {
let r = object!{
"has_bug" => false
};
return r.pretty(2);
}
// 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();
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,
"txid" => txid,
};
return r.pretty(2);
}
}

24
lib/src/lightwallet/data.rs

@ -267,7 +267,7 @@ impl Utxo {
let mut address_bytes = vec![0; address_len as usize];
reader.read_exact(&mut address_bytes)?;
let address = String::from_utf8(address_bytes).unwrap();
assert_eq!(address.chars().take(1).collect::<Vec<char>>()[0], 'R');
assert_eq!(address.chars().take(1).collect::<Vec<char>>()[0], 't');
let mut txid_bytes = [0; 32];
reader.read_exact(&mut txid_bytes)?;
@ -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,
@ -386,17 +390,19 @@ pub struct WalletTx {
// All outgoing sapling sends to addresses outside this wallet
pub outgoing_metadata: Vec<OutgoingTxMetadata>,
// Whether this TxID was downloaded from the server and scanned for Memos
pub full_tx_scanned: bool,
}
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![],
@ -413,6 +419,12 @@ impl WalletTx {
let block = reader.read_i32::<LittleEndian>()?;
let datetime = if version >= 4 {
reader.read_u64::<LittleEndian>()?
} else {
0
};
let mut txid_bytes = [0u8; 32];
reader.read_exact(&mut txid_bytes)?;
@ -431,6 +443,7 @@ impl WalletTx {
Ok(WalletTx{
block,
datetime,
txid,
notes,
utxos,
@ -446,6 +459,8 @@ impl WalletTx {
writer.write_i32::<LittleEndian>(self.block)?;
writer.write_u64::<LittleEndian>(self.datetime)?;
writer.write_all(&self.txid.0)?;
Vector::write(&mut writer, &self.notes, |w, nd| nd.write(w))?;
@ -475,7 +490,8 @@ pub struct SpendableNote {
impl SpendableNote {
pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &ExtendedSpendingKey) -> Option<Self> {
// 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 {

20
lib/src/lightwallet/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 '{}.komodo/HUSH3/' .",
user,
home);
} else {
eprintln!("User {} must have permission to write to '{}/.komodo/HUSH3/ .",
user,
home);
}
}

20
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 '{}.komodo/HUSH3/' .",
user,
home);
} else {
eprintln!("User {} must have permission to write to '{}/.komodo/HUSH3/ .",
user,
home);
}
}

18
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 silentdragonlitelib::{commands,
use silentdragonlitelib::{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;
}
};
@ -137,6 +142,10 @@ pub fn main() {
error!("{}", e);
}
}
// Save before exit
command_tx.send(("save".to_string(), vec![])).unwrap();
resp_rx.recv().unwrap();
}
}
@ -151,7 +160,10 @@ fn startup(server: http::Uri, dangerous: bool, seed: Option<String>, 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

Loading…
Cancel
Save