Browse Source

Merge pull request #31 from MyHush/danger

Danger to dev
dev
Denio 4 years ago
committed by GitHub
parent
commit
313b702b18
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Cargo.lock
  2. 5
      lib/Cargo.toml
  3. 141
      lib/src/commands.rs
  4. 2
      lib/src/lib.rs
  5. 396
      lib/src/lightclient.rs
  6. 617
      lib/src/lightwallet.rs
  7. 130
      lib/src/lightwallet/bugs.rs
  8. 9
      lib/src/lightwallet/data.rs
  9. 585
      lib/src/lightwallet/walletzkey.rs

1
Cargo.lock

@ -1744,6 +1744,7 @@ version = "0.1.0"
dependencies = [
"base58 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bellman 0.1.0 (git+https://github.com/MyHush/librustzcash.git?rev=1a0204113d487cdaaf183c2967010e5214ff9e37)",
"bs58 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
"dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)",

5
lib/Cargo.toml

@ -3,8 +3,13 @@ name = "silentdragonlitelib"
version = "0.1.0"
edition = "2018"
[features]
default = ["embed_params"]
embed_params = []
[dependencies]
base58 = "0.1.0"
bs58 = { version = "0.2", features = ["check"] }
log = "0.4"
log4rs = "0.8.3"
dirs = "2.0.2"

141
lib/src/commands.rs

@ -332,16 +332,6 @@ impl Command for EncryptCommand {
return self.help();
}
// Refuse to encrypt if the bip39 bug has not been fixed
use crate::lightwallet::bugs::BugBip39Derivation;
if BugBip39Derivation::has_bug(lightclient) {
let mut h = vec![];
h.push("It looks like your wallet has the bip39bug. Please run 'fixbip39bug' to fix it");
h.push("before encrypting your wallet.");
h.push("ERROR: Cannot encrypt while wallet has the bip39bug.");
return h.join("\n");
}
let passwd = args[0].to_string();
match lightclient.wallet.write().unwrap().encrypt(passwd) {
@ -658,13 +648,102 @@ impl Command for TransactionsCommand {
}
}
struct ImportCommand {}
impl Command for ImportCommand {
fn help(&self) -> String {
let mut h = vec![];
h.push("Import an external spending or viewing key into the wallet");
h.push("Usage:");
h.push("import <spending_key | viewing_key> <birthday> [norescan]");
h.push("");
h.push("Birthday is the earliest block number that has transactions belonging to the imported key. Rescanning will start from this block. If not sure, you can specify '0', which will start rescanning from the first sapling block.");
h.push("Note that you can import only the full spending (private) key or the full viewing key.");
h.join("\n")
}
fn short_help(&self) -> String {
"Import spending or viewing keys into the wallet".to_string()
}
fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
if args.len() < 1 || args.len() > 2 {
return format!("Insufficient arguments\n\n{}", self.help());
}
let key = args[0];
let rescan = if args.len() == 3 {
if args[2] == "norescan" || args[2] == "false" || args[2] == "no" {
false
} else {
return format!("Couldn't undestand the argument '{}'. Please pass 'norescan' to prevent rescanning the wallet", args[2]);
}
} else {
true
};
let r = match lightclient.do_import_key(key.to_string(), 0) {
Ok(r) => r.pretty(2),
Err(e) => return format!("Error: {}", e),
};
if rescan {
match lightclient.do_rescan() {
Ok(_) => {},
Err(e) => return format!("Error: Rescan failed: {}", e),
};
}
return r;
}
}
struct TImportCommand {}
impl Command for TImportCommand {
fn help(&self) -> String {
let mut h = vec![];
h.push("Import an external WIF");
h.push("Usage:");
h.push("timport wif (Begins with U");
h.push("");
h.join("\n")
}
fn short_help(&self) -> String {
"Import wif to the wallet".to_string()
}
fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
let key = args[0];
let r = match lightclient.do_import_tk(key.to_string()){
Ok(r) => r.pretty(2),
Err(e) => return format!("Error: {}", e),
};
match lightclient.do_rescan() {
Ok(_) => {},
Err(e) => return format!("Error: Rescan failed: {}", e),
};
return r;
}
}
struct HeightCommand {}
impl Command for HeightCommand {
fn help(&self) -> String {
let mut h = vec![];
h.push("Get the latest block height that the wallet is at.");
h.push("Usage:");
h.push("height [do_sync = true | false]");
h.push("height");
h.push("");
h.push("Pass 'true' (default) to sync to the server to get the latest block height. Pass 'false' to get the latest height in the wallet without checking with the server.");
@ -675,21 +754,11 @@ impl Command for HeightCommand {
"Get the latest block height that the wallet is at".to_string()
}
fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
if args.len() > 1 {
return format!("Didn't understand arguments\n{}", self.help());
}
if args.len() == 0 || (args.len() == 1 && args[0].trim() == "true") {
match lightclient.do_sync(true) {
Ok(_) => format!("{}", object! { "height" => lightclient.last_scanned_height()}.pretty(2)),
Err(e) => e
}
} else {
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
format!("{}", object! { "height" => lightclient.last_scanned_height()}.pretty(2))
}
}
}
struct NewAddressCommand {}
@ -794,29 +863,6 @@ 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 {
fn help(&self) -> String {
@ -853,6 +899,8 @@ pub fn get_commands() -> Box<HashMap<String, Box<dyn Command>>> {
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("import".to_string(), Box::new(ImportCommand{}));
map.insert("timport".to_string(), Box::new(TImportCommand{}));
map.insert("export".to_string(), Box::new(ExportCommand{}));
map.insert("info".to_string(), Box::new(InfoCommand{}));
map.insert("coinsupply".to_string(), Box::new(CoinsupplyCommand{}));
@ -868,7 +916,6 @@ pub fn get_commands() -> Box<HashMap<String, Box<dyn Command>>> {
map.insert("decrypt".to_string(), Box::new(DecryptCommand{}));
map.insert("unlock".to_string(), Box::new(UnlockCommand{}));
map.insert("lock".to_string(), Box::new(LockCommand{}));
map.insert("fixbip39bug".to_string(), Box::new(FixBip39BugCommand{}));
Box::new(map)
}

2
lib/src/lib.rs

@ -6,7 +6,7 @@ pub mod grpcconnector;
pub mod lightwallet;
pub mod commands;
#[cfg(feature = "embed_params")]
#[derive(RustEmbed)]
#[folder = "zcash-params/"]
pub struct SaplingParams;

396
lib/src/lightclient.rs

@ -1,12 +1,10 @@
use crate::lightwallet::LightWallet;
use rand::{rngs::OsRng, seq::SliceRandom};
use std::sync::{Arc, RwLock, Mutex, mpsc::channel};
use std::sync::atomic::{AtomicI32, AtomicUsize, Ordering};
use std::path::{Path, PathBuf};
use std::fs::File;
use std::collections::HashMap;
use std::collections::{HashSet, HashMap};
use std::cmp::{max, min};
use std::io;
use std::io::prelude::*;
@ -14,14 +12,11 @@ use std::io::{BufReader, BufWriter, Error, ErrorKind};
use protobuf::parse_from_bytes;
use threadpool::ThreadPool;
use json::{object, array, JsonValue};
use zcash_primitives::transaction::{TxId, Transaction};
use zcash_client_backend::{
constants::testnet, constants::mainnet, constants::regtest, encoding::encode_payment_address,
};
use zcash_client_backend::{constants::testnet, constants::mainnet, constants::regtest,};
use log::{info, warn, error, LevelFilter};
use log4rs::append::rolling_file::RollingFileAppender;
@ -35,7 +30,7 @@ use log4rs::append::rolling_file::policy::compound::{
};
use crate::grpcconnector::{self, *};
use crate::SaplingParams;
use crate::ANCHOR_OFFSET;
@ -187,6 +182,24 @@ impl LightClientConfig {
return self.get_wallet_path().exists()
}
pub fn get_zcash_params_path(&self) -> io::Result<Box<Path>> {
let mut zcash_params = self.get_zcash_data_path().into_path_buf();
zcash_params.push("..");
if cfg!(target_os="macos") || cfg!(target_os="windows") {
zcash_params.push("ZcashParams");
} else {
zcash_params.push(".zcash-params");
}
match std::fs::create_dir_all(zcash_params.clone()) {
Ok(_) => Ok(zcash_params.into_boxed_path()),
Err(e) => {
eprintln!("Couldn't create zcash params directory\n{}", e);
Err(e)
}
}
}
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);
@ -239,6 +252,15 @@ impl LightClientConfig {
}
}
pub fn hrp_sapling_viewing_key(&self) -> &str {
match &self.chain_name[..] {
"main" => mainnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY,
"test" => testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY,
"regtest" => regtest::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY,
c => panic!("Unknown chain {}", c)
}
}
pub fn base58_pubkey_address(&self) -> [u8; 1] {
match &self.chain_name[..] {
"main" => mainnet::B58_PUBKEY_ADDRESS_PREFIX,
@ -260,7 +282,7 @@ impl LightClientConfig {
pub fn base58_secretkey_prefix(&self) -> [u8; 1] {
match &self.chain_name[..] {
"main" => [0x80],
"main" => [0xBC],
"test" => [0xEF],
"regtest" => [0xEF],
c => panic!("Unknown chain {}", c)
@ -294,11 +316,63 @@ impl LightClient {
};
}
fn write_file_if_not_exists(dir: &Box<Path>, name: &str, bytes: &[u8]) -> io::Result<()> {
let mut file_path = dir.to_path_buf();
file_path.push(name);
if !file_path.exists() {
let mut file = File::create(&file_path)?;
file.write_all(bytes)?;
}
Ok(())
}
#[cfg(feature = "embed_params")]
fn read_sapling_params(&mut self) {
// Read Sapling Params
use crate::SaplingParams;
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 set_sapling_params(&mut self, sapling_output: &[u8], sapling_spend: &[u8]) -> Result<(), String> {
use sha2::{Sha256, Digest};
// The hashes of the params need to match
const SAPLING_OUTPUT_HASH: &str = "2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4";
const SAPLING_SPEND_HASH: &str = "8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13";
if SAPLING_OUTPUT_HASH.to_string() != hex::encode(Sha256::digest(&sapling_output)) {
return Err(format!("sapling-output hash didn't match. expected {}, found {}", SAPLING_OUTPUT_HASH, hex::encode(Sha256::digest(&sapling_output)) ))
}
if SAPLING_SPEND_HASH.to_string() != hex::encode(Sha256::digest(&sapling_spend)) {
return Err(format!("sapling-spend hash didn't match. expected {}, found {}", SAPLING_SPEND_HASH, hex::encode(Sha256::digest(&sapling_spend)) ))
}
// Will not overwrite previous params
if self.sapling_output.is_empty() {
self.sapling_output.extend_from_slice(sapling_output);
}
if self.sapling_spend.is_empty() {
self.sapling_spend.extend_from_slice(sapling_spend);
}
// Ensure that the sapling params are stored on disk properly as well.
match self.config.get_zcash_params_path() {
Ok(zcash_params_dir) => {
// Create the sapling output and spend params files
match LightClient::write_file_if_not_exists(&zcash_params_dir, "sapling-output.params", &self.sapling_output) {
Ok(_) => {},
Err(e) => eprintln!("Warning: Couldn't write the output params!\n{}", e)
};
match LightClient::write_file_if_not_exists(&zcash_params_dir, "sapling-spend.params", &self.sapling_spend) {
Ok(_) => {},
Err(e) => eprintln!("Warning: Couldn't write the output params!\n{}", e)
}
},
Err(e) => {
eprintln!("{}", e);
}
};
Ok(())
}
/// Method to create a test-only version of the LightClient
@ -315,6 +389,8 @@ impl LightClient {
};
l.set_wallet_initial_state(0);
#[cfg(feature = "embed_params")]
l.read_sapling_params();
info!("Created new wallet!");
@ -341,6 +417,8 @@ impl LightClient {
};
l.set_wallet_initial_state(latest_block);
#[cfg(feature = "embed_params")]
l.read_sapling_params();
info!("Created new wallet with a new seed!");
@ -367,6 +445,7 @@ impl LightClient {
println!("Setting birthday to {}", birthday);
l.set_wallet_initial_state(birthday);
#[cfg(feature = "embed_params")]
l.read_sapling_params();
println!("Setting Number to {}", number);
@ -386,7 +465,7 @@ impl LightClient {
let mut file_buffer = BufReader::new(File::open(config.get_wallet_path())?);
let wallet = LightWallet::read(&mut file_buffer, config)?;
let mut lc = LightClient {
let mut lc = LightClient {
wallet : Arc::new(RwLock::new(wallet)),
config : config.clone(),
sapling_output : vec![],
@ -395,17 +474,12 @@ impl LightClient {
sync_status : Arc::new(RwLock::new(WalletStatus::new())),
};
#[cfg(feature = "embed_params")]
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/silentdragonlite-light-cli/blob/master/bip39bug.md");
info!("{}", m);
println!("{}", m);
}
Ok(lc)
}
@ -500,11 +574,12 @@ impl LightClient {
let wallet = self.wallet.read().unwrap();
// Go over all z addresses
let z_keys = wallet.get_z_private_keys().iter()
.filter( move |(addr, _)| address.is_none() || address.as_ref() == Some(addr))
.map( |(addr, pk)|
.filter( move |(addr, _, _)| address.is_none() || address.as_ref() == Some(addr))
.map( |(addr, pk, vk)|
object!{
"address" => addr.clone(),
"private_key" => pk.clone()
"private_key" => pk.clone(),
"viewing_key" => vk.clone(),
}
).collect::<Vec<JsonValue>>();
@ -532,12 +607,10 @@ impl LightClient {
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)
}).collect::<Vec<String>>();
let z_addresses = wallet.get_all_zaddresses();
// Collect t addresses
let t_addresses = wallet.taddresses.read().unwrap().iter().map( |a| a.clone() )
let t_addresses = wallet.get_all_taddresses().iter().map( |a| a.clone() )
.collect::<Vec<String>>();
object!{
@ -550,17 +623,17 @@ impl LightClient {
let wallet = self.wallet.read().unwrap();
// Collect z addresses
let z_addresses = wallet.zaddress.read().unwrap().iter().map( |ad| {
let address = encode_payment_address(self.config.hrp_sapling_address(), &ad);
let z_addresses = wallet.get_all_zaddresses().iter().map(|zaddress| {
object!{
"address" => address.clone() ,
"zbalance" => wallet.zbalance(Some(address.clone())) ,
"verified_zbalance" => wallet.verified_zbalance(Some(address)) ,
"address" => zaddress.clone(),
"zbalance" => wallet.zbalance(Some(zaddress.clone())),
"verified_zbalance" => wallet.verified_zbalance(Some(zaddress.clone())),
"spendable_zbalance" => wallet.spendable_zbalance(Some(zaddress.clone()))
}
}).collect::<Vec<JsonValue>>();
// Collect t addresses
let t_addresses = wallet.taddresses.read().unwrap().iter().map( |address| {
let t_addresses = wallet.get_all_taddresses().iter().map( |address| {
// Get the balance for this address
let balance = wallet.tbalance(Some(address.clone())) ;
@ -573,6 +646,7 @@ impl LightClient {
object!{
"zbalance" => wallet.zbalance(None),
"verified_zbalance" => wallet.verified_zbalance(None),
"spendable_zbalance" => wallet.spendable_zbalance(None),
"tbalance" => wallet.tbalance(None),
"z_addresses" => z_addresses,
"t_addresses" => t_addresses,
@ -682,22 +756,39 @@ impl LightClient {
let mut spent_notes : Vec<JsonValue> = vec![];
let mut pending_notes: Vec<JsonValue> = vec![];
let anchor_height: i32 = self.wallet.read().unwrap().get_anchor_height() as i32;
{
// Collect Sapling notes
let wallet = self.wallet.read().unwrap();
// First, collect all extfvk's that are spendable (i.e., we have the private key)
let spendable_address: HashSet<String> = wallet.get_all_zaddresses().iter()
.filter(|address| wallet.have_spending_key_for_zaddress(address))
.map(|address| address.clone())
.collect();
// Collect Sapling notes
wallet.txs.read().unwrap().iter()
.flat_map( |(txid, wtx)| {
let spendable_address = spendable_address.clone();
wtx.notes.iter().filter_map(move |nd|
if !all_notes && nd.spent.is_some() {
None
} else {
let address = LightWallet::note_address(self.config.hrp_sapling_address(), nd);
let spendable = address.is_some() &&
spendable_address.contains(&address.clone().unwrap()) &&
wtx.block <= anchor_height && nd.spent.is_none() && nd.unconfirmed_spent.is_none();
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),
"address" => address,
"spendable" => spendable,
"spent" => nd.spent.map(|spent_txid| format!("{}", spent_txid)),
"unconfirmed_spent" => nd.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)),
})
@ -895,7 +986,7 @@ impl LightClient {
let new_address = {
let wallet = self.wallet.write().unwrap();
match addr_type {
let addr = match addr_type {
"zs" => wallet.add_zaddr(),
"R" => wallet.add_taddr(),
_ => {
@ -903,7 +994,101 @@ impl LightClient {
error!("{}", e);
return Err(e);
}
};
if addr.starts_with("Error") {
let e = format!("Error creating new address: {}", addr);
error!("{}", e);
return Err(e);
}
addr
};
self.do_save()?;
Ok(array![new_address])
}
/// Import a new private key
pub fn do_import_tk(&self, sk: String) -> Result<JsonValue, String> {
if !self.wallet.read().unwrap().is_unlocked_for_spending() {
error!("Wallet is locked");
return Err("Wallet is locked".to_string());
}
let new_address = {
let wallet = self.wallet.write().unwrap();
let addr = wallet.import_taddr(sk);
if addr.starts_with("Error") {
let e = format!("Error creating new address{}", addr);
error!("{}", e);
return Err(e);
}
addr
};
self.do_save()?;
Ok(array![new_address])
}
/// Convinence function to determine what type of key this is and import it
pub fn do_import_key(&self, key: String, birthday: u64) -> Result<JsonValue, String> {
if key.starts_with(self.config.hrp_sapling_private_key()) {
self.do_import_sk(key, birthday)
} else if key.starts_with(self.config.hrp_sapling_viewing_key()) {
self.do_import_vk(key, birthday)
} else {
Err(format!("'{}' was not recognized as either a spending key or a viewing key because it didn't start with either '{}' or '{}'",
key, self.config.hrp_sapling_private_key(), self.config.hrp_sapling_viewing_key()))
}
}
/// Import a new private key
pub fn do_import_sk(&self, sk: String, birthday: u64) -> Result<JsonValue, String> {
if !self.wallet.read().unwrap().is_unlocked_for_spending() {
error!("Wallet is locked");
return Err("Wallet is locked".to_string());
}
let new_address = {
let mut wallet = self.wallet.write().unwrap();
let addr = wallet.add_imported_sk(sk, birthday);
if addr.starts_with("Error") {
let e = format!("Error creating new address{}", addr);
error!("{}", e);
return Err(e);
}
addr
};
self.do_save()?;
Ok(array![new_address])
}
/// Import a new viewing key
pub fn do_import_vk(&self, vk: String, birthday: u64) -> Result<JsonValue, String> {
if !self.wallet.read().unwrap().is_unlocked_for_spending() {
error!("Wallet is locked");
return Err("Wallet is locked".to_string());
}
let new_address = {
let mut wallet = self.wallet.write().unwrap();
let addr = wallet.add_imported_vk(vk, birthday);
if addr.starts_with("Error") {
let e = format!("Error creating new address{}", addr);
error!("{}", e);
return Err(e);
}
addr
};
self.do_save()?;
@ -1141,53 +1326,18 @@ impl LightClient {
// So, reset the total_reorg
total_reorg = 0;
// We'll also fetch all the txids that our transparent addresses are involved with
{
// 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>>();
// Create a channel so the fetch_transparent_txids can send the results back
let (ctx, crx) = channel();
let num_addresses = addresses.len();
for address in addresses {
let wallet = self.wallet.clone();
let block_times_inner = block_times.clone();
// If this is the first pass after a retry, fetch older t address txids too, becuse
// they might have been missed last time.
let transparent_start_height = if pass == 1 && retry_count > 0 {
start_height - scan_batch_size
} else {
start_height
};
// If this is the first pass after a retry, fetch older t address txids too, becuse
// they might have been missed last time.
let transparent_start_height = if pass == 1 && retry_count > 0 {
start_height - scan_batch_size
} else {
start_height
};
let pool = pool.clone();
let server_uri = self.get_server_uri();
let ctx = ctx.clone();
let no_cert = self.config.no_cert_verification;
pool.execute(move || {
// Fetch the transparent transactions for this address, and send the results
// via the channel
let r = fetch_transparent_txids(&server_uri, address, transparent_start_height, end_height, no_cert,
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);
});
ctx.send(r).unwrap();
});
}
let no_cert = true;
// Collect all results from the transparent fetches, and make sure everything was OK.
// If it was not, we return an error, which will go back to the retry
crx.iter().take(num_addresses).collect::<Result<Vec<()>, String>>()?;
}
// We'll also fetch all the txids that our transparent addresses are involved with
self.scan_taddress_txids(&pool, block_times, transparent_start_height, end_height, no_cert)?;
// Do block height accounting
last_scanned_height = end_height;
@ -1214,6 +1364,61 @@ impl LightClient {
// Get the Raw transaction for all the wallet transactions
{
let decoy_txids = all_new_txs.read().unwrap();
match self.scan_fill_fulltxs(&pool, decoy_txids.to_vec()) {
Ok(_) => Ok(object!{
"result" => "success",
"latest_block" => latest_block,
"downloaded_bytes" => bytes_downloaded.load(Ordering::SeqCst)
}),
Err(e) => Err(format!("Error fetching all txns for memos: {}", e))
}
}
}
fn scan_taddress_txids(&self, pool: &ThreadPool, block_times: Arc<RwLock<HashMap<u64, u32>>>, start_height: u64, end_height: u64, no_cert: bool) -> Result<Vec<()>, String> {
// 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()
.get_all_taddresses().iter()
.map(|a| a.clone())
.collect::<Vec<String>>();
// Create a channel so the fetch_transparent_txids can send the results back
let (ctx, crx) = channel();
let num_addresses = addresses.len();
for address in addresses {
let wallet = self.wallet.clone();
let pool = pool.clone();
let server_uri = self.get_server_uri();
let ctx = ctx.clone();
let block_times = block_times.clone();
pool.execute(move || {
// Fetch the transparent transactions for this address, and send the results
// via the channel
let r = fetch_transparent_txids(&server_uri, address, start_height, end_height,no_cert,
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.read().unwrap().scan_full_tx(&tx, height as i32, datetime as u64);
});
ctx.send(r).unwrap();
});
}
// Collect all results from the transparent fetches, and make sure everything was OK.
// If it was not, we return an error, which will go back to the retry
crx.iter().take(num_addresses).collect::<Result<Vec<()>, String>>()
}
fn scan_fill_fulltxs(&self, pool: &ThreadPool, decoy_txids: Vec<(TxId, i32)>) -> Result<Vec<()>, String> {
// 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.read().unwrap().txs.read().unwrap().values()
@ -1221,14 +1426,11 @@ impl LightClient {
.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()[..]);
info!("Fetching {} new txids, total {} with decoy", txids_to_fetch.len(), decoy_txids.len());
txids_to_fetch.extend_from_slice(&decoy_txids[..]);
txids_to_fetch.sort();
txids_to_fetch.dedup();
let mut rng = OsRng;
txids_to_fetch.shuffle(&mut rng);
let num_fetches = txids_to_fetch.len();
let (ctx, crx) = channel();
@ -1258,15 +1460,7 @@ impl LightClient {
};
// Wait for all the fetches to finish.
let result = crx.iter().take(num_fetches).collect::<Result<Vec<()>, String>>();
match result {
Ok(_) => Ok(object!{
"result" => "success",
"latest_block" => latest_block,
"downloaded_bytes" => bytes_downloaded.load(Ordering::SeqCst)
}),
Err(e) => Err(format!("Error fetching all txns for memos: {}", e))
}
crx.iter().take(num_fetches).collect::<Result<Vec<()>, String>>()
}
pub fn do_send(&self, addrs: Vec<(&str, u64, Option<String>)>) -> Result<String, String> {
@ -1324,11 +1518,15 @@ pub mod tests {
assert!(!lc.do_export(None).is_err());
assert!(!lc.do_seed_phrase().is_err());
// This will lock the wallet again, so after this, we'll need to unlock again
assert!(!lc.do_new_address("R").is_err());
lc.wallet.write().unwrap().unlock("password".to_string()).unwrap();
// Can't add keys while unlocked but encrypted
assert!(lc.do_new_address("R").is_err());
assert!(lc.do_new_address("zs").is_err());
// Remove encryption, which will allow adding
lc.wallet.write().unwrap().remove_encryption("password".to_string()).unwrap();
assert!(!lc.do_new_address("zs").is_err());
assert!(lc.do_new_address("R").is_ok());
assert!(lc.do_new_address("zs").is_ok());
}
#[test]
@ -1380,6 +1578,14 @@ pub mod tests {
}
}
#[test]
pub fn test_bad_import() {
let lc = super::LightClient::unconnected(TEST_SEED.to_string(), None).unwrap();
assert!(lc.do_import_sk("bad_priv_key".to_string(), 0).is_err());
assert!(lc.do_import_vk("bad_view_key".to_string(), 0).is_err());
}
#[test]
pub fn test_wallet_creation() {
// Create a new tmp director

617
lib/src/lightwallet.rs

@ -23,8 +23,10 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use pairing::bls12_381::{Bls12};
use sha2::{Sha256, Digest};
use sodiumoxide::crypto::secretbox;
use zcash_client_backend::{
encoding::{encode_payment_address, encode_extended_spending_key},
encoding::{encode_payment_address, encode_extended_spending_key, encode_extended_full_viewing_key, decode_extended_spending_key, decode_extended_full_viewing_key},
proto::compact_formats::{CompactBlock, CompactOutput},
wallet::{WalletShieldedOutput, WalletShieldedSpend}
};
@ -59,10 +61,11 @@ mod extended_key;
mod utils;
mod address;
mod prover;
pub mod bugs;
mod walletzkey;
use data::{BlockData, WalletTx, Utxo, SaplingNoteData, SpendableNote, OutgoingTxMetadata};
use extended_key::{KeyIndex, ExtendedPrivKey};
use walletzkey::{WalletZKey, WalletTKey, WalletZKeyType};
pub const MAX_REORG: usize = 100;
pub const GAP_RULE_UNUSED_ADDRESSES: usize = 5;
@ -117,18 +120,12 @@ pub struct LightWallet {
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>>>,
// List of keys, actually in this wallet. This is a combination of HD keys derived from the seed,
// viewing keys and imported spending keys.
zkeys: Arc<RwLock<Vec<WalletZKey>>>,
pub zaddress: Arc<RwLock<Vec<PaymentAddress<Bls12>>>>,
// 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>>>,
// Transparent keys.
tkeys: Arc<RwLock<Vec<WalletTKey>>>,
blocks: Arc<RwLock<Vec<BlockData>>>,
pub txs: Arc<RwLock<HashMap<TxId, WalletTx>>>,
@ -149,7 +146,7 @@ pub struct LightWallet {
impl LightWallet {
pub fn serialized_version() -> u64 {
return 6;
return 9;
}
fn get_taddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> SecretKey {
@ -244,8 +241,9 @@ impl LightWallet {
// 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
let (extsk, extfvk, address)
= LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0);
let hdkey_num = 0;
let (extsk, _, _)
= LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed.as_bytes(), hdkey_num);
let lw = LightWallet {
encrypted: false,
@ -253,11 +251,8 @@ impl LightWallet {
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])),
zkeys: Arc::new(RwLock::new(vec![WalletZKey::new_hdkey(hdkey_num, extsk)])),
tkeys: Arc::new(RwLock::new(vec![WalletTKey::new_hdkey(tpk, taddr)])),
blocks: Arc::new(RwLock::new(vec![])),
txs: Arc::new(RwLock::new(HashMap::new())),
mempool_txs: Arc::new(RwLock::new(HashMap::new())),
@ -320,34 +315,84 @@ impl LightWallet {
let mut seed_bytes = [0u8; 32];
reader.read_exact(&mut seed_bytes)?;
// Read the spending keys
let extsks = Vector::read(&mut reader, |r| ExtendedSpendingKey::read(r))?;
let zkeys = if version <= 6 {
// Up until version 6, the wallet keys were written out individually
// Read the spending keys
let extsks = Vector::read(&mut reader, |r| ExtendedSpendingKey::read(r))?;
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>>()
};
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 )
.collect::<Vec<PaymentAddress<Bls12>>>();
// If extsks is of len 0, then this wallet is locked
let zkeys_result = if extsks.len() == 0 {
// Wallet is locked, so read only the viewing keys.
extfvks.iter().zip(addresses.iter()).enumerate().map(|(i, (extfvk, payment_address))| {
let zk = WalletZKey::new_locked_hdkey(i as u32, extfvk.clone());
if zk.zaddress != *payment_address {
Err(io::Error::new(ErrorKind::InvalidData, "Payment address didn't match"))
} else {
Ok(zk)
}
}).collect::<Vec<io::Result<WalletZKey>>>()
} else {
// Wallet is unlocked, read the spending keys as well
extsks.into_iter().zip(extfvks.into_iter().zip(addresses.iter())).enumerate()
.map(|(i, (extsk, (extfvk, payment_address)))| {
let zk = WalletZKey::new_hdkey(i as u32, extsk);
if zk.zaddress != *payment_address {
return Err(io::Error::new(ErrorKind::InvalidData, "Payment address didn't match"));
}
if zk.extfvk != extfvk {
return Err(io::Error::new(ErrorKind::InvalidData, "Full View key didn't match"));
}
Ok(zk)
}).collect::<Vec<io::Result<WalletZKey>>>()
};
// Convert vector of results into result of vector, returning an error if any one of the keys failed the checks above
zkeys_result.into_iter().collect::<io::Result<_>>()?
} else {
// After version 6, we read the WalletZKey structs directly
Vector::read(&mut reader, |r| WalletZKey::read(r))?
};
// Calculate the addresses
let addresses = extfvks.iter().map( |fvk| fvk.default_address().unwrap().1 )
.collect::<Vec<PaymentAddress<Bls12>>>();
let tkeys = Vector::read(&mut reader, |r| {
let mut tpk_bytes = [0u8; 32];
r.read_exact(&mut tpk_bytes)?;
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))?
let wallet_tkeys = if version >= 9 {
Vector::read(&mut reader, |r| {
WalletTKey::read(r)
})?
} else {
// Calculate the addresses
tkeys.iter().map(|sk| LightWallet::address_from_prefix_sk(&config.base58_pubkey_address(), sk)).collect()
let tkeys = Vector::read(&mut reader, |r| {
let mut tpk_bytes = [0u8; 32];
r.read_exact(&mut tpk_bytes)?;
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()
};
tkeys.iter().zip(taddresses.iter()).map(|(k, a)|
WalletTKey::new_hdkey(*k, a.clone())
).collect()
};
let blocks = Vector::read(&mut reader, |r| BlockData::read(r))?;
@ -375,11 +420,8 @@ impl LightWallet {
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)),
zkeys: Arc::new(RwLock::new(zkeys)),
tkeys: Arc::new(RwLock::new(wallet_tkeys)),
blocks: Arc::new(RwLock::new(blocks)),
txs: Arc::new(RwLock::new(txs)),
mempool_txs: Arc::new(RwLock::new(HashMap::new())),
@ -413,24 +455,14 @@ impl LightWallet {
// Flush after writing the seed, so in case of a disaster, we can still recover the seed.
writer.flush()?;
// Write all the spending keys
Vector::write(&mut writer, &self.extsks.read().unwrap(),
|w, sk| sk.write(w)
)?;
// Write the FVKs
Vector::write(&mut writer, &self.extfvks.read().unwrap(),
|w, fvk| fvk.write(w)
// Write all the wallet's keys
Vector::write(&mut writer, &self.zkeys.read().unwrap(),
|w, zk| zk.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)
|w, tk| tk.write(w)
)?;
Vector::write(&mut writer, &self.blocks.read().unwrap(), |w, b| b.write(w))?;
@ -484,21 +516,32 @@ impl LightWallet {
.unwrap_or(&cmp::max(self.birthday, self.config.sapling_activation_height))
}
// Get all z-address private keys. Returns a Vector of (address, privatekey)
pub fn get_z_private_keys(&self) -> Vec<(String, String)> {
self.extsks.read().unwrap().iter().map(|sk| {
(encode_payment_address(self.config.hrp_sapling_address(),
&ExtendedFullViewingKey::from(sk).default_address().unwrap().1),
encode_extended_spending_key(self.config.hrp_sapling_private_key(), &sk)
)
}).collect::<Vec<(String, String)>>()
// Get all z-address private keys. Returns a Vector of (address, privatekey, viewkey)
pub fn get_z_private_keys(&self) -> Vec<(String, String, String)> {
let keys = self.zkeys.read().unwrap().iter().map(|k| {
let pkey = match k.extsk.clone().map(|extsk| encode_extended_spending_key(self.config.hrp_sapling_private_key(), &extsk)) {
Some(pk) => pk,
None => "".to_string()
};
let vkey = encode_extended_full_viewing_key(self.config.hrp_sapling_viewing_key(), &k.extfvk);
(encode_payment_address(self.config.hrp_sapling_address(),&k.zaddress), pkey, vkey)
}).collect::<Vec<(String, String, String)>>();
keys
}
/// 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.tkeys.read().unwrap().iter().map(|wtk| {
let sk = if wtk.tkey.is_some() {
wtk.tkey.unwrap()[..].to_base58check(&self.config.base58_secretkey_prefix(), &[0x01])
} else {
"".to_string()
};
(wtk.address.clone(), sk)
}).collect::<Vec<(String, String)>>()
}
@ -507,21 +550,30 @@ impl LightWallet {
/// NOTE: This does NOT rescan
pub fn add_zaddr(&self) -> String {
if !self.unlocked {
return "".to_string();
return "Error: Can't add key while wallet is locked".to_string();
}
let pos = self.extsks.read().unwrap().len() as u32;
if self.encrypted {
return "Error: Can't add key while wallet is encrypted".to_string();
}
// Find the highest pos we have
let pos = self.zkeys.read().unwrap().iter()
.filter(|zk| zk.hdkey_num.is_some())
.max_by(|zk1, zk2| zk1.hdkey_num.unwrap().cmp(&zk2.hdkey_num.unwrap()))
.map_or(0, |zk| zk.hdkey_num.unwrap() + 1);
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), "");
let (extsk, extfvk, address) =
let (extsk, _, _) =
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.zaddress.write().unwrap().push(address);
// let zaddr = encode_payment_address(self.config.hrp_sapling_address(), &address);
let newkey = WalletZKey::new_hdkey(pos, extsk);
self.zkeys.write().unwrap().push(newkey.clone());
zaddr
encode_payment_address(self.config.hrp_sapling_address(), &newkey.zaddress)
}
// Add a new Sietch Addr. This will derive a new zdust address from manipluated seed
@ -555,10 +607,14 @@ impl LightWallet {
/// Add a new t address to the wallet. This will derive a new address from the seed
/// at the next position.
/// NOTE: This is not rescan the wallet
/// NOTE: This will not rescan the wallet
pub fn add_taddr(&self) -> String {
if !self.unlocked {
return "".to_string();
return "Error: Can't add key while wallet is locked".to_string();
}
if self.encrypted {
return "Error: Can't add key while wallet is encrypted".to_string();
}
let pos = self.tkeys.read().unwrap().len() as u32;
@ -567,12 +623,118 @@ impl LightWallet {
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.tkeys.write().unwrap().push(WalletTKey::new_hdkey(sk, address.clone()));
address
}
pub fn import_taddr(&self, sk: String) -> String {
if !self.unlocked {
return "Error: Can't add key while wallet is locked".to_string();
}
//// Decode Wif to base58 to hex
let sk_to_bs58 = bs58::decode(sk).into_vec().unwrap();
let bs58_to_hex = hex::encode(sk_to_bs58);
//// Manipulate string, to exclude last 4 bytes (checksum bytes), first 2 bytes (secretkey prefix) and the compressed flag (works only for compressed Wifs!)
let slice_sk = &bs58_to_hex[2..66];
//// Get the SecretKey from slice
let secret_key = SecretKey::from_slice(&hex::decode(slice_sk).unwrap());
let sk_raw = secret_key.unwrap();
//// Make sure the key doesn't already exist
if self.tkeys.read().unwrap().iter().find(|&wk| wk.tkey.is_some() && wk.tkey.as_ref().unwrap() == &sk_raw.clone()).is_some() {
return "Error: Key already exists".to_string();
}
//// Get the taddr from key
let address = self.address_from_sk(&sk_raw);
//// Add to tkeys
self.tkeys.write().unwrap().push(WalletTKey::import_hdkey(sk_raw , address.clone()));
address
}
// Add a new imported spending key to the wallet
/// NOTE: This will not rescan the wallet
pub fn add_imported_sk(&mut self, sk: String, birthday: u64) -> String {
if !self.unlocked {
return "Error: Can't add key while wallet is locked".to_string();
}
// First, try to interpret the key
let extsk = match decode_extended_spending_key(self.config.hrp_sapling_private_key(), &sk) {
Ok(Some(k)) => k,
Ok(None) => return format!("Error: Couldn't decode spending key"),
Err(e) => return format!("Error importing spending key: {}", e)
};
// Make sure the key doesn't already exist
if self.zkeys.read().unwrap().iter().find(|&wk| wk.extsk.is_some() && wk.extsk.as_ref().unwrap() == &extsk.clone()).is_some() {
return "Error: Key already exists".to_string();
}
let extfvk = ExtendedFullViewingKey::from(&extsk);
let zaddress = {
let mut zkeys = self.zkeys.write().unwrap();
let maybe_existing_zkey = zkeys.iter_mut().find(|wk| wk.extfvk == extfvk);
// If the viewing key exists, and is now being upgraded to the spending key, replace it in-place
if maybe_existing_zkey.is_some() {
let mut existing_zkey = maybe_existing_zkey.unwrap();
existing_zkey.extsk = Some(extsk);
existing_zkey.keytype = WalletZKeyType::ImportedSpendingKey;
existing_zkey.zaddress.clone()
} else {
let newkey = WalletZKey::new_imported_sk(extsk);
zkeys.push(newkey.clone());
newkey.zaddress
}
};
// Adjust wallet birthday
if birthday < self.birthday {
self.birthday = if birthday < self.config.sapling_activation_height {self.config.sapling_activation_height} else {birthday};
}
encode_payment_address(self.config.hrp_sapling_address(), &zaddress)
}
// Add a new imported viewing key to the wallet
/// NOTE: This will not rescan the wallet
pub fn add_imported_vk(&mut self, vk: String, birthday: u64) -> String {
if !self.unlocked {
return "Error: Can't add key while wallet is locked".to_string();
}
// First, try to interpret the key
let extfvk = match decode_extended_full_viewing_key(self.config.hrp_sapling_viewing_key(), &vk) {
Ok(Some(k)) => k,
Ok(None) => return format!("Error: Couldn't decode viewing key"),
Err(e) => return format!("Error importing viewing key: {}", e)
};
// Make sure the key doesn't already exist
if self.zkeys.read().unwrap().iter().find(|wk| wk.extfvk == extfvk.clone()).is_some() {
return "Error: Key already exists".to_string();
}
let newkey = WalletZKey::new_imported_viewkey(extfvk);
self.zkeys.write().unwrap().push(newkey.clone());
// Adjust wallet birthday
if birthday < self.birthday {
self.birthday = if birthday < self.config.sapling_activation_height {self.config.sapling_activation_height} else {birthday};
}
encode_payment_address(self.config.hrp_sapling_address(), &newkey.zaddress)
}
/// Clears all the downloaded blocks and resets the state back to the initial block.
/// After this, the wallet's initial state will need to be set
/// and the wallet will need to be rescanned
@ -666,6 +828,26 @@ impl LightWallet {
}
}
/// Get the height of the anchor block
pub fn get_anchor_height(&self) -> u32 {
match self.get_target_height_and_anchor_offset() {
Some((height, anchor_offset)) => height - anchor_offset as u32 - 1,
None => return 0,
}
}
pub fn get_all_taddresses(&self) -> Vec<String> {
self.tkeys.read().unwrap()
.iter()
.map(|wtx| wtx.address.clone()).collect()
}
pub fn get_all_zaddresses(&self) -> Vec<String> {
self.zkeys.read().unwrap().iter().map( |zk| {
encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress)
}).collect()
}
pub fn memo_str(memo: &Option<Memo>) -> Option<String> {
match memo {
Some(memo) => {
@ -716,7 +898,7 @@ impl LightWallet {
}
pub fn encrypt(&mut self, passwd: String) -> io::Result<()> {
use sodiumoxide::crypto::secretbox;
if self.encrypted {
return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is already encrypted"));
@ -729,8 +911,17 @@ impl LightWallet {
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.nonce = nonce.as_ref().to_vec();
// Encrypt the individual keys
self.tkeys.write().unwrap().iter_mut()
.map(|k| k.encrypt(&key))
.collect::<io::Result<Vec<()>>>()?;
self.zkeys.write().unwrap().iter_mut()
.map(|k| k.encrypt(&key))
.collect::<io::Result<Vec<()>>>()?;
self.encrypted = true;
self.lock()?;
@ -749,8 +940,16 @@ impl LightWallet {
// 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![]));
// Remove all the private key from the tkeys
self.tkeys.write().unwrap().iter_mut().map(|tk| {
tk.lock()
}).collect::<io::Result<Vec<_>>>()?;
// Remove all the private key from the zkeys
self.zkeys.write().unwrap().iter_mut().map(|zk| {
zk.lock()
}).collect::<io::Result<Vec<_>>>()?;
self.unlocked = false;
@ -758,7 +957,7 @@ impl LightWallet {
}
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"));
@ -784,43 +983,17 @@ impl LightWallet {
// 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);
// Go over the tkeys, and add the keys again
self.tkeys.write().unwrap().iter_mut().map(|tk| {
tk.unlock(&key)
}).collect::<io::Result<Vec<()>>>()?;
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);
}
// Go over the zkeys, and add the spending keys again
self.zkeys.write().unwrap().iter_mut().map(|zk| {
zk.unlock(&self.config, bip39_seed.as_bytes(), &key)
}).collect::<io::Result<Vec<()>>>()?;
// 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;
@ -840,6 +1013,16 @@ impl LightWallet {
if !self.unlocked {
self.unlock(passwd)?;
}
// Remove encryption from individual tkeys
self.tkeys.write().unwrap().iter_mut().map(|tk| {
tk.remove_encryption()
}).collect::<io::Result<Vec<()>>>()?;
// Remove encryption from individual zkeys
self.zkeys.write().unwrap().iter_mut().map(|zk| {
zk.remove_encryption()
}).collect::<io::Result<Vec<()>>>()?;
// Permanantly remove the encryption
self.encrypted = false;
@ -917,6 +1100,7 @@ impl LightWallet {
if tx.block as u32 <= anchor_height {
tx.notes
.iter()
.filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none())
.filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it.
match addr.clone() {
Some(a) => a == encode_payment_address(
@ -927,7 +1111,42 @@ impl LightWallet {
None => true
}
})
.map(|nd| if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { nd.note.value } else { 0 })
.map(|nd| nd.note.value)
.sum::<u64>()
} else {
0
}
})
.sum::<u64>()
}
pub fn spendable_zbalance(&self, addr: Option<String>) -> u64 {
let anchor_height = self.get_anchor_height();
self.txs
.read()
.unwrap()
.values()
.map(|tx| {
if tx.block as u32 <= anchor_height {
tx.notes
.iter()
.filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none())
.filter(|nd| {
// Check to see if we have this note's spending key.
self.have_spendingkey_for_extfvk(&nd.extfvk)
})
.filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it.
match addr.clone() {
Some(a) => a == encode_payment_address(
self.config.hrp_sapling_address(),
&nd.extfvk.fvk.vk
.into_payment_address(nd.diversifier, &JUBJUB).unwrap()
),
None => true
}
})
.map(|nd| nd.note.value)
.sum::<u64>()
} else {
0
@ -936,6 +1155,24 @@ impl LightWallet {
.sum::<u64>() as u64
}
pub fn have_spendingkey_for_extfvk(&self, extfvk: &ExtendedFullViewingKey) -> bool {
match self.zkeys.read().unwrap().iter().find(|zk| zk.extfvk == *extfvk) {
None => false,
Some(zk) => zk.have_spending_key()
}
}
pub fn have_spending_key_for_zaddress(&self, address: &String) -> bool {
match self.zkeys.read().unwrap().iter()
.find(|zk| encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress) == *address)
{
None => false,
Some(zk) => zk.have_spending_key()
}
}
fn add_toutput_to_wtx(&self, height: i32, timestamp: u64, txid: &TxId, vout: &TxOut, n: u64) {
let mut txs = self.txs.write().unwrap();
@ -978,7 +1215,12 @@ impl LightWallet {
// If one of the last 'n' taddress was used, ensure we add the next HD taddress to the wallet.
pub fn ensure_hd_taddresses(&self, address: &String) {
let last_addresses = {
self.taddresses.read().unwrap().iter().rev().take(GAP_RULE_UNUSED_ADDRESSES).map(|s| s.clone()).collect::<Vec<String>>()
self.tkeys.read().unwrap()
.iter()
.map(|t| t.address.clone())
.rev().take(GAP_RULE_UNUSED_ADDRESSES).map(|s|
s.clone())
.collect::<Vec<String>>()
};
match last_addresses.iter().position(|s| *s == *address) {
@ -1001,8 +1243,11 @@ impl LightWallet {
// If one of the last 'n' zaddress was used, ensure we add the next HD zaddress to the wallet
pub fn ensure_hd_zaddresses(&self, address: &String) {
let last_addresses = {
self.zaddress.read().unwrap().iter().rev().take(GAP_RULE_UNUSED_ADDRESSES)
.map(|s| encode_payment_address(self.config.hrp_sapling_address(), s))
self.zkeys.read().unwrap().iter()
.filter(|zk| zk.keytype == WalletZKeyType::HdKey)
.rev()
.take(GAP_RULE_UNUSED_ADDRESSES)
.map(|s| encode_payment_address(self.config.hrp_sapling_address(), &s.zaddress))
.collect::<Vec<String>>()
};
@ -1068,7 +1313,8 @@ impl LightWallet {
}
// Scan for t outputs
let all_taddresses = self.taddresses.read().unwrap().iter()
let all_taddresses = self.tkeys.read().unwrap().iter()
.map(|wtx| wtx.address.clone())
.map(|a| a.clone())
.collect::<Vec<_>>();
for address in all_taddresses {
@ -1095,7 +1341,8 @@ impl LightWallet {
// outgoing metadata
// Collect our t-addresses
let wallet_taddrs = self.taddresses.read().unwrap().iter()
let wallet_taddrs = self.tkeys.read().unwrap().iter()
.map(|wtx| wtx.address.clone())
.map(|a| a.clone())
.collect::<HashSet<String>>();
@ -1130,18 +1377,18 @@ impl LightWallet {
// Scan shielded sapling outputs to see if anyone of them is us, and if it is, extract the memo
for output in tx.shielded_outputs.iter() {
let ivks: Vec<_> = self.extfvks.read().unwrap().iter().map(
|extfvk| extfvk.fvk.vk.ivk().clone()
).collect();
let ivks: Vec<_> = self.zkeys.read().unwrap().iter()
.map(|zk| zk.extfvk.fvk.vk.ivk()
).collect();
let cmu = output.cmu;
let ct = output.enc_ciphertext;
// Search all of our keys
for (_account, ivk) in ivks.iter().enumerate() {
for ivk in ivks {
let epk_prime = output.ephemeral_key.as_prime_order(&JUBJUB).unwrap();
let (note, _to, memo) = match try_sapling_note_decryption(ivk, &epk_prime, &cmu, &ct) {
let (note, _to, memo) = match try_sapling_note_decryption(&ivk, &epk_prime, &cmu, &ct) {
Some(ret) => ret,
None => continue,
};
@ -1173,17 +1420,18 @@ impl LightWallet {
// First, collect all our z addresses, to check for change
// Collect z addresses
let z_addresses = self.zaddress.read().unwrap().iter().map( |ad| {
encode_payment_address(self.config.hrp_sapling_address(), &ad)
let z_addresses = self.zkeys.read().unwrap().iter().map( |zk| {
encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress)
}).collect::<HashSet<String>>();
// Search all ovks that we have
let ovks: Vec<_> = self.extfvks.read().unwrap().iter().map(
|extfvk| extfvk.fvk.ovk.clone()
).collect();
let ovks: Vec<_> = self.zkeys.read().unwrap().iter()
.map(|zk| zk.extfvk.fvk.ovk.clone())
.collect();
for (_account, ovk) in ovks.iter().enumerate() {
match try_sapling_output_recovery(ovk,
for ovk in ovks {
match try_sapling_output_recovery(
&ovk,
&output.cv,
&output.cmu,
&output.ephemeral_key.as_prime_order(&JUBJUB).unwrap(),
@ -1617,17 +1865,19 @@ impl LightWallet {
new_txs = {
let nf_refs = nfs.iter().map(|(nf, account, _)| (nf.to_vec(), *account)).collect::<Vec<_>>();
let extfvks: Vec<ExtendedFullViewingKey> = self.zkeys.read().unwrap().iter().map(|zk| zk.extfvk.clone()).collect();
// Create a single mutable slice of all the newly-added witnesses.
let mut witness_refs: Vec<_> = txs
.values_mut()
.map(|tx| tx.notes.iter_mut().filter_map(|nd| nd.witnesses.last_mut()))
.map(|tx| tx.notes.iter_mut().filter_map(
|nd| if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { nd.witnesses.last_mut() } else { None }))
.flatten()
.collect();
self.scan_block_internal(
block.clone(),
&self.extfvks.read().unwrap(),
&extfvks,
nf_refs,
&mut block_data.tree,
&mut witness_refs[..],
@ -1691,7 +1941,7 @@ impl LightWallet {
// Save notes.
for output in tx.shielded_outputs
{
let new_note = SaplingNoteData::new(&self.extfvks.read().unwrap()[output.account], output);
let new_note = SaplingNoteData::new(&self.zkeys.read().unwrap()[output.account].extfvk, output);
match LightWallet::note_address(self.config.hrp_sapling_address(), &new_note) {
Some(a) => {
info!("Received sapling output to {}", a);
@ -1806,29 +2056,33 @@ impl LightWallet {
println!("{}: Selecting notes", now() - start_time);
let target_value = Amount::from_u64(total_value).unwrap() + DEFAULT_FEE ;
// Select the candidate notes that are eligible to be spent
let mut candidate_notes: Vec<_> = self.txs.read().unwrap().iter()
.map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note)))
.flatten()
.filter_map(|(txid, note)|
SpendableNote::from(txid, note, anchor_offset, &self.extsks.read().unwrap()[note.account])
).collect();
// Sort by highest value-notes first.
candidate_notes.sort_by(|a, b| b.note.value.cmp(&a.note.value));
// Select the minimum number of notes required to satisfy the target value
let notes: Vec<_> = candidate_notes.iter()
.scan(0, |running_total, spendable| {
let value = spendable.note.value;
let ret = if *running_total < u64::from(target_value) {
Some(spendable)
} else {
None
};
*running_total = *running_total + value;
ret
})
.collect();
let notes: Vec<_> = self.txs.read().unwrap().iter()
.map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note)))
.flatten()
.filter_map(|(txid, note)| {
// Filter out notes that are already spent
if note.spent.is_some() || note.unconfirmed_spent.is_some() {
None
} else {
// Get the spending key for the selected fvk, if we have it
let extsk = self.zkeys.read().unwrap().iter()
.find(|zk| zk.extfvk == note.extfvk)
.and_then(|zk| zk.extsk.clone());
SpendableNote::from(txid, note, anchor_offset, &extsk)
}
})
.scan(0, |running_total, spendable| {
let value = spendable.note.value;
let ret = if *running_total < u64::from(target_value) {
Some(spendable)
} else {
None
};
*running_total = *running_total + value;
ret
})
.collect();
let mut builder = Builder::new(height);
@ -1846,7 +2100,8 @@ impl LightWallet {
// Create a map from address -> sk for all taddrs, so we can spend from the
// right address
let address_to_sk = self.tkeys.read().unwrap().iter()
.map(|sk| (self.address_from_sk(&sk), sk.clone()))
.filter(|wtk| wtk.tkey.is_some())
.map(|wtk| (wtk.address.clone(), wtk.tkey.unwrap().clone()))
.collect::<HashMap<_,_>>();
// Add all tinputs
@ -1859,15 +2114,11 @@ impl LightWallet {
script_pubkey: Script { 0: utxo.script.clone() },
};
match address_to_sk.get(&utxo.address) {
Some(sk) => builder.add_transparent_input(*sk, outpoint.clone(), coin.clone()),
None => {
// Something is very wrong
let e = format!("Couldn't find the secreykey for taddr {}", utxo.address);
error!("{}", e);
Err(zcash_primitives::transaction::builder::Error::InvalidAddress)
}
if let Some(sk) = address_to_sk.get(&utxo.address) {
return builder.add_transparent_input(*sk, outpoint.clone(), coin.clone())
} else {
info!("Not adding a UTXO because secret key is absent.");
return Ok(())
}
})
@ -1909,12 +2160,12 @@ impl LightWallet {
// the builder will automatically send change to that address
if notes.len() == 0 {
builder.send_change_to(
ExtendedFullViewingKey::from(&self.extsks.read().unwrap()[0]).fvk.ovk,
self.extsks.read().unwrap()[0].default_address().unwrap().1);
self.zkeys.read().unwrap()[0].extfvk.fvk.ovk,
self.zkeys.read().unwrap()[0].zaddress.clone());
}
// TODO: We're using the first ovk to encrypt outgoing Txns. Is that Ok?
let ovk = self.extfvks.read().unwrap()[0].fvk.ovk;
let ovk = self.zkeys.read().unwrap()[0].extfvk.fvk.ovk;
for (to, value, memo) in recepients {
// Compute memo if it exists

130
lib/src/lightwallet/bugs.rs

@ -1,130 +0,0 @@
///
/// In v1.0 of silentdragonlite-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;
}
if wallet.is_encrypted() {
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 amount: u64 = balance_json["zbalance"].as_u64().unwrap()
+ balance_json["tbalance"].as_u64().unwrap();
let txid = if amount > 0 {
println!("Sending funds to ourself.");
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
match client.do_send(vec![(&zaddr, amount-fee, 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);
}
}

9
lib/src/lightwallet/data.rs

@ -141,8 +141,7 @@ impl SaplingNoteData {
// Reading a note also needs the corresponding address to read from.
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let version = reader.read_u64::<LittleEndian>()?;
assert_eq!(version, SaplingNoteData::serialized_version());
let _version = reader.read_u64::<LittleEndian>()?;
let account = reader.read_u64::<LittleEndian>()? as usize;
@ -488,9 +487,9 @@ pub struct SpendableNote {
}
impl SpendableNote {
pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &ExtendedSpendingKey) -> Option<Self> {
pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &Option<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() && extsk.is_some() &&
nd.witnesses.len() >= (anchor_offset + 1) {
let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1);
@ -500,7 +499,7 @@ impl SpendableNote {
diversifier: nd.diversifier,
note: nd.note.clone(),
witness: w.clone(),
extsk: extsk.clone(),
extsk: extsk.clone().unwrap(),
})
} else {
None

585
lib/src/lightwallet/walletzkey.rs

@ -0,0 +1,585 @@
use std::io::{self, Read, Write};
use std::io::{Error, ErrorKind};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use pairing::bls12_381::{Bls12};
use sodiumoxide::crypto::secretbox;
use zcash_primitives::{
serialize::{Vector, Optional},
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
primitives::{PaymentAddress},
};
use crate::lightclient::{LightClientConfig};
use crate::lightwallet::{LightWallet, utils};
#[derive(PartialEq, Debug, Clone)]
pub enum WalletTKeyType {
HdKey = 0,
ImportedKey = 1,
}
// A struct that holds z-address private keys or view keys
#[derive(Clone, Debug, PartialEq)]
pub struct WalletTKey {
pub(super) keytype: WalletTKeyType,
locked: bool,
pub(super) address: String,
pub(super) tkey: Option<secp256k1::SecretKey>,
// If locked, the encrypted key is here
enc_key: Option<Vec<u8>>,
nonce: Option<Vec<u8>>,
}
impl WalletTKey {
pub fn new_hdkey(key: secp256k1::SecretKey, address: String) -> Self {
WalletTKey {
keytype: WalletTKeyType::HdKey,
locked: false,
address,
tkey: Some(key),
enc_key: None,
nonce: None,
}
}
pub fn import_hdkey(key: secp256k1::SecretKey, address: String) -> Self {
WalletTKey {
keytype: WalletTKeyType::ImportedKey,
locked: false,
address,
tkey: Some(key),
enc_key: None,
nonce: None,
}
}
fn serialized_version() -> u8 {
return 1;
}
pub fn read<R: Read>(mut inp: R) -> io::Result<Self> {
let version = inp.read_u8()?;
assert!(version <= Self::serialized_version());
let keytype: WalletTKeyType = match inp.read_u32::<LittleEndian>()? {
0 => Ok(WalletTKeyType::HdKey),
1 => Ok(WalletTKeyType::ImportedKey),
n => Err(io::Error::new(ErrorKind::InvalidInput, format!("Unknown tkey type {}", n)))
}?;
let locked = inp.read_u8()? > 0;
let address = utils::read_string(&mut inp)?;
let tkey = Optional::read(&mut inp, |r| {
let mut tpk_bytes = [0u8; 32];
r.read_exact(&mut tpk_bytes)?;
secp256k1::SecretKey::from_slice(&tpk_bytes).map_err(|e| io::Error::new(ErrorKind::InvalidData, e))
})?;
let enc_key = Optional::read(&mut inp, |r|
Vector::read(r, |r| r.read_u8()))?;
let nonce = Optional::read(&mut inp, |r|
Vector::read(r, |r| r.read_u8()))?;
Ok(WalletTKey {
keytype,
locked,
address,
tkey,
enc_key,
nonce,
})
}
pub fn write<W: Write>(&self, mut out: W) -> io::Result<()> {
out.write_u8(Self::serialized_version())?;
out.write_u32::<LittleEndian>(self.keytype.clone() as u32)?;
out.write_u8(self.locked as u8)?;
utils::write_string(&mut out, &self.address)?;
Optional::write(&mut out, &self.tkey, |w, pk|
w.write_all(&pk[..])
)?;
// Write enc_key
Optional::write(&mut out, &self.enc_key, |o, v|
Vector::write(o, v, |o,n| o.write_u8(*n)))?;
// Write nonce
Optional::write(&mut out, &self.nonce, |o, v|
Vector::write(o, v, |o,n| o.write_u8(*n)))
}
pub fn lock(&mut self) -> io::Result<()> {
// For keys, encrypt the key into enckey
// assert that we have the encrypted key.
if self.enc_key.is_none() {
return Err(Error::new(ErrorKind::InvalidInput, "Can't lock when t-addr private key is not encrypted"));
}
self.tkey = None;
self.locked = true;
Ok(())
}
pub fn unlock(&mut self, key: &secretbox::Key) -> io::Result<()> {
// For imported keys, we need to decrypt from the encrypted key
let nonce = secretbox::Nonce::from_slice(&self.nonce.as_ref().unwrap()).unwrap();
let sk_bytes = match secretbox::open(&self.enc_key.as_ref().unwrap(), &nonce, &key) {
Ok(s) => s,
Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));}
};
self.tkey = Some(secp256k1::SecretKey::from_slice(&sk_bytes[..]).map_err(|e|
io::Error::new(ErrorKind::InvalidData, format!("{}", e))
)?);
self.locked = false;
Ok(())
}
pub fn encrypt(&mut self, key: &secretbox::Key) -> io::Result<()> {
// For keys, encrypt the key into enckey
let nonce = secretbox::gen_nonce();
let sk_bytes = &self.tkey.unwrap()[..];
self.enc_key = Some(secretbox::seal(&sk_bytes, &nonce, &key));
self.nonce = Some(nonce.as_ref().to_vec());
self.tkey = None;
// Also lock after encrypt
self.lock()
}
pub fn remove_encryption(&mut self) -> io::Result<()> {
if self.locked {
return Err(Error::new(ErrorKind::InvalidInput, "Can't remove encryption while locked"));
}
self.enc_key = None;
self.nonce = None;
Ok(())
}
}
#[derive(PartialEq, Debug, Clone)]
pub enum WalletZKeyType {
HdKey = 0,
ImportedSpendingKey = 1,
ImportedViewKey = 2
}
// A struct that holds z-address private keys or view keys
#[derive(Clone, Debug, PartialEq)]
pub struct WalletZKey {
pub(super) keytype: WalletZKeyType,
locked: bool,
pub(super) extsk: Option<ExtendedSpendingKey>,
pub(super) extfvk: ExtendedFullViewingKey,
pub(super) zaddress: PaymentAddress<Bls12>,
// If this is a HD key, what is the key number
pub(super) hdkey_num: Option<u32>,
// If locked, the encrypted private key is stored here
enc_key: Option<Vec<u8>>,
nonce: Option<Vec<u8>>,
}
impl WalletZKey {
pub fn new_hdkey(hdkey_num: u32, extsk: ExtendedSpendingKey) -> Self {
let extfvk = ExtendedFullViewingKey::from(&extsk);
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::HdKey,
locked: false,
extsk: Some(extsk),
extfvk,
zaddress,
hdkey_num: Some(hdkey_num),
enc_key: None,
nonce: None,
}
}
pub fn new_locked_hdkey(hdkey_num: u32, extfvk: ExtendedFullViewingKey) -> Self {
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::HdKey,
locked: true,
extsk: None,
extfvk,
zaddress,
hdkey_num: Some(hdkey_num),
enc_key: None,
nonce: None
}
}
pub fn new_imported_sk(extsk: ExtendedSpendingKey) -> Self {
let extfvk = ExtendedFullViewingKey::from(&extsk);
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::ImportedSpendingKey,
locked: false,
extsk: Some(extsk),
extfvk,
zaddress,
hdkey_num: None,
enc_key: None,
nonce: None,
}
}
pub fn new_imported_viewkey(extfvk: ExtendedFullViewingKey) -> Self {
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::ImportedViewKey,
locked: false,
extsk: None,
extfvk,
zaddress,
hdkey_num: None,
enc_key: None,
nonce: None,
}
}
fn serialized_version() -> u8 {
return 1;
}
pub fn have_spending_key(&self) -> bool {
self.extsk.is_some() || self.enc_key.is_some() || self.hdkey_num.is_some()
}
pub fn read<R: Read>(mut inp: R) -> io::Result<Self> {
let version = inp.read_u8()?;
assert!(version <= Self::serialized_version());
let keytype: WalletZKeyType = match inp.read_u32::<LittleEndian>()? {
0 => Ok(WalletZKeyType::HdKey),
1 => Ok(WalletZKeyType::ImportedSpendingKey),
2 => Ok(WalletZKeyType::ImportedViewKey),
n => Err(io::Error::new(ErrorKind::InvalidInput, format!("Unknown zkey type {}", n)))
}?;
let locked = inp.read_u8()? > 0;
let extsk = Optional::read(&mut inp, |r| ExtendedSpendingKey::read(r))?;
let extfvk = ExtendedFullViewingKey::read(&mut inp)?;
let zaddress = extfvk.default_address().unwrap().1;
let hdkey_num = Optional::read(&mut inp, |r| r.read_u32::<LittleEndian>())?;
let enc_key = Optional::read(&mut inp, |r|
Vector::read(r, |r| r.read_u8()))?;
let nonce = Optional::read(&mut inp, |r|
Vector::read(r, |r| r.read_u8()))?;
Ok(WalletZKey {
keytype,
locked,
extsk,
extfvk,
zaddress,
hdkey_num,
enc_key,
nonce,
})
}
pub fn write<W: Write>(&self, mut out: W) -> io::Result<()> {
out.write_u8(Self::serialized_version())?;
out.write_u32::<LittleEndian>(self.keytype.clone() as u32)?;
out.write_u8(self.locked as u8)?;
Optional::write(&mut out, &self.extsk, |w, sk| ExtendedSpendingKey::write(sk, w))?;
ExtendedFullViewingKey::write(&self.extfvk, &mut out)?;
Optional::write(&mut out, &self.hdkey_num, |o, n| o.write_u32::<LittleEndian>(*n))?;
// Write enc_key
Optional::write(&mut out, &self.enc_key, |o, v|
Vector::write(o, v, |o,n| o.write_u8(*n)))?;
// Write nonce
Optional::write(&mut out, &self.nonce, |o, v|
Vector::write(o, v, |o,n| o.write_u8(*n)))
}
pub fn lock(&mut self) -> io::Result<()> {
match self.keytype {
WalletZKeyType::HdKey => {
// For HD keys, just empty out the keys, since they will be reconstructed from the hdkey_num
self.extsk = None;
self.locked = true;
},
WalletZKeyType::ImportedSpendingKey => {
// For imported keys, encrypt the key into enckey
// assert that we have the encrypted key.
if self.enc_key.is_none() {
return Err(Error::new(ErrorKind::InvalidInput, "Can't lock when imported key is not encrypted"));
}
self.extsk = None;
self.locked = true;
},
WalletZKeyType::ImportedViewKey => {
// For viewing keys, there is nothing to lock, so just return true
self.locked = true;
}
}
Ok(())
}
pub fn unlock(&mut self, config: &LightClientConfig, bip39_seed: &[u8], key: &secretbox::Key) -> io::Result<()> {
match self.keytype {
WalletZKeyType::HdKey => {
let (extsk, extfvk, address) =
LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed, self.hdkey_num.unwrap());
if address != self.zaddress {
return Err(io::Error::new(ErrorKind::InvalidData,
format!("zaddress mismatch at {}. {:?} vs {:?}", self.hdkey_num.unwrap(), address, self.zaddress)));
}
if extfvk != self.extfvk {
return Err(io::Error::new(ErrorKind::InvalidData,
format!("fvk mismatch at {}. {:?} vs {:?}", self.hdkey_num.unwrap(), extfvk, self.extfvk)));
}
self.extsk = Some(extsk);
},
WalletZKeyType::ImportedSpendingKey => {
// For imported keys, we need to decrypt from the encrypted key
let nonce = secretbox::Nonce::from_slice(&self.nonce.as_ref().unwrap()).unwrap();
let extsk_bytes = match secretbox::open(&self.enc_key.as_ref().unwrap(), &nonce, &key) {
Ok(s) => s,
Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));}
};
self.extsk = Some(ExtendedSpendingKey::read(&extsk_bytes[..])?);
},
WalletZKeyType::ImportedViewKey => {
// Viewing key unlocking is basically a no op
}
};
self.locked = false;
Ok(())
}
pub fn encrypt(&mut self, key: &secretbox::Key) -> io::Result<()> {
match self.keytype {
WalletZKeyType::HdKey => {
// For HD keys, we don't need to do anything, since the hdnum has all the info to recreate this key
},
WalletZKeyType::ImportedSpendingKey => {
// For imported keys, encrypt the key into enckey
let nonce = secretbox::gen_nonce();
let mut sk_bytes = vec![];
self.extsk.as_ref().unwrap().write(&mut sk_bytes)?;
self.enc_key = Some(secretbox::seal(&sk_bytes, &nonce, &key));
self.nonce = Some(nonce.as_ref().to_vec());
},
WalletZKeyType::ImportedViewKey => {
// Encrypting a viewing key is a no-op
}
}
// Also lock after encrypt
self.lock()
}
pub fn remove_encryption(&mut self) -> io::Result<()> {
if self.locked {
return Err(Error::new(ErrorKind::InvalidInput, "Can't remove encryption while locked"));
}
match self.keytype {
WalletZKeyType::HdKey => {
// For HD keys, we don't need to do anything, since the hdnum has all the info to recreate this key
Ok(())
},
WalletZKeyType::ImportedSpendingKey => {
self.enc_key = None;
self.nonce = None;
Ok(())
},
WalletZKeyType::ImportedViewKey => {
// Removing encryption is a no-op for viewing keys
Ok(())
}
}
}
}
#[cfg(test)]
pub mod tests {
use zcash_client_backend::{
encoding::{encode_payment_address, decode_extended_spending_key, decode_extended_full_viewing_key}
};
use sodiumoxide::crypto::secretbox;
use crate::lightclient::LightClientConfig;
use super::WalletZKey;
fn get_config() -> LightClientConfig {
LightClientConfig {
server: "0.0.0.0:0".parse().unwrap(),
chain_name: "main".to_string(),
sapling_activation_height: 0,
consensus_branch_id: "000000".to_string(),
anchor_offset: 0,
data_dir: None,
}
}
#[test]
fn test_serialize() {
let config = get_config();
// Priv Key's address is "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv"
let privkey = "secret-extended-key-main1q0p44m9zqqqqpqyxfvy5w2vq6ahvxyrwsk2w4h2zleun4cft4llmnsjlv77lhuuknv6x9jgu5g2clf3xq0wz9axxxq8klvv462r5pa32gjuj5uhxnvps6wsrdg6xll05unwks8qpgp4psmvy5e428uxaggn4l29duk82k3sv3njktaaj453fdmfmj2fup8rls4egqxqtj2p5a3yt4070khn99vzxj5ag5qjngc4v2kq0ctl9q2rpc2phu4p3e26egu9w88mchjf83sqgh3cev";
let esk = decode_extended_spending_key(config.hrp_sapling_private_key(), privkey).unwrap().unwrap();
let wzk = WalletZKey::new_imported_sk(esk);
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv".to_string());
let mut v: Vec<u8> = vec![];
// Serialize
wzk.write(&mut v).unwrap();
// Read it right back
let wzk2 = WalletZKey::read(&v[..]).unwrap();
{
assert_eq!(wzk, wzk2);
assert_eq!(wzk.extsk, wzk2.extsk);
assert_eq!(wzk.extfvk, wzk2.extfvk);
assert_eq!(wzk.zaddress, wzk2.zaddress);
}
}
#[test]
fn test_encrypt_decrypt_sk() {
let config = get_config();
// Priv Key's address is "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv"
let privkey = "secret-extended-key-main1q0p44m9zqqqqpqyxfvy5w2vq6ahvxyrwsk2w4h2zleun4cft4llmnsjlv77lhuuknv6x9jgu5g2clf3xq0wz9axxxq8klvv462r5pa32gjuj5uhxnvps6wsrdg6xll05unwks8qpgp4psmvy5e428uxaggn4l29duk82k3sv3njktaaj453fdmfmj2fup8rls4egqxqtj2p5a3yt4070khn99vzxj5ag5qjngc4v2kq0ctl9q2rpc2phu4p3e26egu9w88mchjf83sqgh3cev";
let esk = decode_extended_spending_key(config.hrp_sapling_private_key(), privkey).unwrap().unwrap();
let mut wzk = WalletZKey::new_imported_sk(esk);
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv".to_string());
// Can't lock without encryption
assert!(wzk.lock().is_err());
// Encryption key
let key = secretbox::Key::from_slice(&[0; 32]).unwrap();
// Encrypt, but save the extsk first
let orig_extsk = wzk.extsk.clone().unwrap();
wzk.encrypt(&key).unwrap();
{
assert!(wzk.enc_key.is_some());
assert!(wzk.nonce.is_some());
}
// Now lock
assert!(wzk.lock().is_ok());
{
assert!(wzk.extsk.is_none());
assert_eq!(wzk.locked, true);
assert_eq!(wzk.zaddress, wzk.extfvk.default_address().unwrap().1);
}
// Can't remove encryption without unlocking
assert!(wzk.remove_encryption().is_err());
// Unlock
assert!(wzk.unlock(&config, &[], &key).is_ok());
{
assert_eq!(wzk.extsk, Some(orig_extsk));
}
// Remove encryption
assert!(wzk.remove_encryption().is_ok());
{
assert_eq!(wzk.enc_key, None);
assert_eq!(wzk.nonce, None);
}
}
#[test]
fn test_encrypt_decrypt_vk() {
let config = get_config();
// Priv Key's address is "zs1va5902apnzlhdu0pw9r9q7ca8s4vnsrp2alr6xndt69jnepn2v2qrj9vg3wfcnjyks5pg65g9dc"
let viewkey = "zxviews1qvvx7cqdqyqqpqqte7292el2875kw2fgvnkmlmrufyszlcy8xgstwarnumqye3tr3d9rr3ydjm9zl9464majh4pa3ejkfy779dm38sfnkar67et7ykxkk0z9rfsmf9jclfj2k85xt2exkg4pu5xqyzyxzlqa6x3p9wrd7pwdq2uvyg0sal6zenqgfepsdp8shestvkzxuhm846r2h3m4jvsrpmxl8pfczxq87886k0wdasppffjnd2eh47nlmkdvrk6rgyyl0ekh3ycqtvvje";
let extfvk = decode_extended_full_viewing_key(config.hrp_sapling_viewing_key(), viewkey).unwrap().unwrap();
let mut wzk = WalletZKey::new_imported_viewkey(extfvk);
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1va5902apnzlhdu0pw9r9q7ca8s4vnsrp2alr6xndt69jnepn2v2qrj9vg3wfcnjyks5pg65g9dc".to_string());
// Encryption key
let key = secretbox::Key::from_slice(&[0; 32]).unwrap();
// Encrypt
wzk.encrypt(&key).unwrap();
{
assert!(wzk.enc_key.is_none());
assert!(wzk.nonce.is_none());
}
// Now lock
assert!(wzk.lock().is_ok());
{
assert!(wzk.extsk.is_none());
assert_eq!(wzk.locked, true);
assert_eq!(wzk.zaddress, wzk.extfvk.default_address().unwrap().1);
}
// Can't remove encryption without unlocking
assert!(wzk.remove_encryption().is_err());
// Unlock
assert!(wzk.unlock(&config, &[], &key).is_ok());
{
assert_eq!(wzk.extsk, None);
}
// Remove encryption
assert!(wzk.remove_encryption().is_ok());
{
assert_eq!(wzk.enc_key, None);
assert_eq!(wzk.nonce, None);
}
}
}
Loading…
Cancel
Save