From e358448e4549c76b60bedbfafdffdbf54cef382c Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Thu, 3 Oct 2019 11:21:02 -0700 Subject: [PATCH 1/8] RwLock for secret keys --- .gitignore | 3 +- src/commands.rs | 29 +++++- src/lightclient.rs | 37 +++++--- src/lightwallet.rs | 219 ++++++++++++++++++++++++++------------------- src/main.rs | 6 +- 5 files changed, 189 insertions(+), 105 deletions(-) diff --git a/.gitignore b/.gitignore index 453e1ec..b2c4e23 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ Cargo.lock .vscode/ history.txt /.idea/ -tarpaulin-report.html \ No newline at end of file +tarpaulin-report.html +/log* \ No newline at end of file diff --git a/src/commands.rs b/src/commands.rs index 3188137..ed087a9 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -139,7 +139,7 @@ impl Command for BalanceCommand { "Show the current TAZ balance in the wallet".to_string() } - fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { + fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { lightclient.do_sync(true); format!("{}", lightclient.do_balance().pretty(2)) @@ -311,6 +311,32 @@ impl Command for TransactionsCommand { } } +struct NewAddressCommand {} +impl Command for NewAddressCommand { + fn help(&self) -> String { + let mut h = vec![]; + h.push("Create a new address in this wallet"); + h.push("Usage:"); + h.push("new [z | t]"); + h.push(""); + h.push("Example:"); + h.push("To create a new z address:"); + h.push("new z"); + h.join("\n") + } + + fn short_help(&self) -> String { + "Create a new address in this wallet".to_string() + } + + fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { + if args.len() != 1 { + return format!("No address type specified\n{}", self.help()); + } + + format!("{}", lightclient.do_new_address(args[0]).pretty(2)) + } +} struct NotesCommand {} impl Command for NotesCommand { @@ -388,6 +414,7 @@ pub fn get_commands() -> Box>> { 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{})); Box::new(map) diff --git a/src/lightclient.rs b/src/lightclient.rs index 1032889..dadf375 100644 --- a/src/lightclient.rs +++ b/src/lightclient.rs @@ -10,7 +10,7 @@ use std::io; use std::io::prelude::*; use std::io::{BufReader, BufWriter, Error, ErrorKind}; -use json::{object, JsonValue}; +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, @@ -190,7 +190,7 @@ impl LightClient { let wallet = LightWallet::read(&mut file_buffer, config)?; LightClient { - wallet : Arc::new(wallet), + wallet : Arc::new(wallet), config : config.clone(), sapling_output : vec![], sapling_spend : vec![] @@ -225,7 +225,7 @@ impl LightClient { } // Export private keys - pub fn do_export(&self, addr: Option) -> json::JsonValue { + pub fn do_export(&self, addr: Option) -> JsonValue { // Clone address so it can be moved into the closure let address = addr.clone(); @@ -259,14 +259,14 @@ impl LightClient { all_keys.into() } - pub fn do_address(&self) -> json::JsonValue { + pub fn do_address(&self) -> JsonValue { // Collect z addresses - let z_addresses = self.wallet.address.iter().map( |ad| { + let z_addresses = self.wallet.address.read().unwrap().iter().map( |ad| { encode_payment_address(self.config.hrp_sapling_address(), &ad) }).collect::>(); // Collect t addresses - let t_addresses = self.wallet.tkeys.iter().map( |sk| { + let t_addresses = self.wallet.tkeys.read().unwrap().iter().map( |sk| { self.wallet.address_from_sk(&sk) }).collect::>(); @@ -276,9 +276,9 @@ impl LightClient { } } - pub fn do_balance(&self) -> json::JsonValue { + pub fn do_balance(&self) -> JsonValue { // Collect z addresses - let z_addresses = self.wallet.address.iter().map( |ad| { + let z_addresses = self.wallet.address.read().unwrap().iter().map( |ad| { let address = encode_payment_address(self.config.hrp_sapling_address(), &ad); object!{ "address" => address.clone(), @@ -288,7 +288,7 @@ impl LightClient { }).collect::>(); // Collect t addresses - let t_addresses = self.wallet.tkeys.iter().map( |sk| { + let t_addresses = self.wallet.tkeys.read().unwrap().iter().map( |sk| { let address = self.wallet.address_from_sk(&sk); // Get the balance for this address @@ -516,6 +516,23 @@ impl LightClient { JsonValue::Array(tx_list) } + /// Create a new address, deriving it from the seed. + pub fn do_new_address(&self, addr_type: &str) -> JsonValue { + let new_address = match addr_type { + "z" => self.wallet.add_zaddr(), + "t" => self.wallet.add_taddr(), + _ => { + let e = format!("Unrecognized address type: {}", addr_type); + error!("{}", e); + return object!{ + "error" => e + }; + } + }; + + array![new_address] + } + pub fn do_rescan(&self) -> String { info!("Rescan starting"); // First, clear the state from the wallet @@ -630,7 +647,7 @@ impl LightClient { // 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[0]); + 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, move |tx_bytes: &[u8], height: u64 | { diff --git a/src/lightwallet.rs b/src/lightwallet.rs index 3813a00..089b6db 100644 --- a/src/lightwallet.rs +++ b/src/lightwallet.rs @@ -9,6 +9,7 @@ use log::{info, warn, error}; use protobuf::parse_from_bytes; +use secp256k1::SecretKey; use bip39::{Mnemonic, Language}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; @@ -62,7 +63,7 @@ pub fn double_sha256(payload: &[u8]) -> Vec { h2.to_vec() } -use base58::{ToBase58, FromBase58}; +use base58::{ToBase58}; /// A trait for converting a [u8] to base58 encoded string. pub trait ToBase58Check { @@ -85,25 +86,25 @@ impl ToBase58Check for [u8] { payload.to_base58() } } - -pub trait FromBase58Check { - fn from_base58check(&self, version: &[u8], suffix: &[u8]) -> Vec; -} - - -impl FromBase58Check for str { - fn from_base58check(&self, version: &[u8], suffix: &[u8]) -> Vec { - let mut payload: Vec = Vec::new(); - let bytes = self.from_base58().unwrap(); - - let start = version.len(); - let end = bytes.len() - (4 + suffix.len()); - - payload.extend(&bytes[start..end]); - - payload - } -} +// +//pub trait FromBase58Check { +// fn from_base58check(&self, version: &[u8], suffix: &[u8]) -> Vec; +//} +// +// +//impl FromBase58Check for str { +// fn from_base58check(&self, version: &[u8], suffix: &[u8]) -> Vec { +// let mut payload: Vec = Vec::new(); +// let bytes = self.from_base58().unwrap(); +// +// let start = version.len(); +// let end = bytes.len() - (4 + suffix.len()); +// +// payload.extend(&bytes[start..end]); +// +// payload +// } +//} pub struct LightWallet { @@ -112,12 +113,12 @@ pub struct LightWallet { // 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 - extsks: Vec, - extfvks: Vec, - pub address: Vec>, + extsks: Arc>>, + extfvks: Arc>>, + pub address: Arc>>>, // Transparent keys. TODO: Make it not pubic - pub tkeys: Vec, + pub tkeys: Arc>>, blocks: Arc>>, pub txs: Arc>>, @@ -135,14 +136,26 @@ impl LightWallet { return 3; } - fn get_pk_from_bip39seed(config: LightClientConfig, bip39seed: &[u8]) -> + fn get_taddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> SecretKey { + let ext_t_key = ExtendedPrivKey::with_seed(bip39_seed).unwrap(); + ext_t_key + .derive_private_key(KeyIndex::hardened_from_normalize_index(44).unwrap()).unwrap() + .derive_private_key(KeyIndex::hardened_from_normalize_index(config.get_coin_type()).unwrap()).unwrap() + .derive_private_key(KeyIndex::hardened_from_normalize_index(0).unwrap()).unwrap() + .derive_private_key(KeyIndex::Normal(0)).unwrap() + .derive_private_key(KeyIndex::Normal(pos)).unwrap() + .private_key + } + + + fn get_zaddr_from_bip39seed(config: &LightClientConfig, bip39seed: &[u8], pos: u32) -> (ExtendedSpendingKey, ExtendedFullViewingKey, PaymentAddress) { let extsk: ExtendedSpendingKey = ExtendedSpendingKey::from_path( &ExtendedSpendingKey::master(bip39seed), &[ ChildIndex::Hardened(32), ChildIndex::Hardened(config.get_coin_type()), - ChildIndex::Hardened(0) + ChildIndex::Hardened(pos) ], ); let extfvk = ExtendedFullViewingKey::from(&extsk); @@ -170,27 +183,20 @@ 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(), ""); - // TODO: This only reads one key for now - let ext_t_key = ExtendedPrivKey::with_seed(&bip39_seed.as_bytes()).unwrap(); - let tpk = ext_t_key - .derive_private_key(KeyIndex::hardened_from_normalize_index(44).unwrap()).unwrap() - .derive_private_key(KeyIndex::hardened_from_normalize_index(config.get_coin_type()).unwrap()).unwrap() - .derive_private_key(KeyIndex::hardened_from_normalize_index(0).unwrap()).unwrap() - .derive_private_key(KeyIndex::Normal(0)).unwrap() - .derive_private_key(KeyIndex::Normal(0)).unwrap() - .private_key; - // Derive only the first address + let tpk = LightWallet::get_taddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0); + // 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_pk_from_bip39seed(config.clone(), &bip39_seed.as_bytes()); + let (extsk, extfvk, address) + = LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0); Ok(LightWallet { seed: seed_bytes, - extsks: vec![extsk], - extfvks: vec![extfvk], - address: vec![address], - tkeys: vec![tpk], + 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(), @@ -245,10 +251,10 @@ impl LightWallet { Ok(LightWallet{ seed: seed_bytes, - extsks, - extfvks, - address: addresses, - tkeys: tkeys, + 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(), @@ -264,12 +270,12 @@ impl LightWallet { writer.write_all(&self.seed)?; // Write all the spending keys - Vector::write(&mut writer, &self.extsks, + Vector::write(&mut writer, &self.extsks.read().unwrap(), |w, sk| sk.write(w) )?; // Write the transparent private key - Vector::write(&mut writer, &self.tkeys, + Vector::write(&mut writer, &self.tkeys.read().unwrap(), |w, pk| w.write_all(&pk[..]) )?; @@ -317,7 +323,7 @@ impl LightWallet { // Get all z-address private keys. Returns a Vector of (address, privatekey) pub fn get_z_private_keys(&self) -> Vec<(String, String)> { - self.extsks.iter().map(|sk| { + 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) @@ -325,16 +331,44 @@ impl LightWallet { }).collect::>() } - // Get all t-address private keys. Returns a Vector of (address, secretkey) + /// Get all t-address private keys. Returns a Vector of (address, secretkey) pub fn get_t_secret_keys(&self) -> Vec<(String, String)> { - self.tkeys.iter().map(|sk| { + self.tkeys.read().unwrap().iter().map(|sk| { (self.address_from_sk(sk), sk[..].to_base58check(&self.config.base58_secretkey_prefix(), &[0x01])) }).collect::>() } - // 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 + /// Adds a new z address to the wallet. This will derive a new address from the seed + /// at the next position and add it to the wallet. + /// NOTE: This does NOT rescan + pub fn add_zaddr(&self) -> String { + let pos = self.extsks.read().unwrap().len() as u32; + let (extsk, extfvk, address) = + LightWallet::get_zaddr_from_bip39seed(&self.config, &self.seed, 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); + + zaddr + } + + /// 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 + pub fn add_taddr(&self) -> String { + let pos = self.tkeys.read().unwrap().len() as u32; + let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &self.seed, pos); + + self.tkeys.write().unwrap().push(sk); + + self.address_from_sk(&sk) + } + + /// 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 pub fn clear_blocks(&self) { self.blocks.write().unwrap().clear(); self.txs.write().unwrap().clear(); @@ -590,7 +624,7 @@ impl LightWallet { // TODO: Iterate over all transparent addresses. This is currently looking only at // the first one. - let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &self.tkeys[0]).serialize(); + let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &self.tkeys.read().unwrap()[0]).serialize(); let mut total_transparent_spend: u64 = 0; @@ -653,7 +687,7 @@ impl LightWallet { // outgoing metadata // Collect our t-addresses - let wallet_taddrs = self.tkeys.iter() + let wallet_taddrs = self.tkeys.read().unwrap().iter() .map(|sk| self.address_from_sk(sk)) .collect::>(); @@ -688,7 +722,9 @@ 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.iter().map(|extfvk| extfvk.fvk.vk.ivk()).collect(); + let ivks: Vec<_> = self.extfvks.read().unwrap().iter().map( + |extfvk| extfvk.fvk.vk.ivk().clone() + ).collect(); let cmu = output.cmu; let ct = output.enc_ciphertext; @@ -720,12 +756,15 @@ impl LightWallet { // First, collect all our z addresses, to check for change // Collect z addresses - let z_addresses = self.address.iter().map( |ad| { + let z_addresses = self.address.read().unwrap().iter().map( |ad| { encode_payment_address(self.config.hrp_sapling_address(), &ad) }).collect::>(); // Search all ovks that we have - let ovks: Vec<_> = self.extfvks.iter().map(|extfvk| extfvk.fvk.ovk).collect(); + let ovks: Vec<_> = self.extfvks.read().unwrap().iter().map( + |extfvk| extfvk.fvk.ovk.clone() + ).collect(); + for (_account, ovk) in ovks.iter().enumerate() { match try_sapling_output_recovery(ovk, &output.cv, @@ -921,7 +960,7 @@ impl LightWallet { scan_block( block, - &self.extfvks, + &self.extfvks.read().unwrap(), &nf_refs[..], &mut block_data.tree, &mut witness_refs[..], @@ -971,7 +1010,7 @@ impl LightWallet { { info!("Received sapling output"); - let new_note = SaplingNoteData::new(&self.extfvks[output.account], output); + let new_note = SaplingNoteData::new(&self.extfvks.read().unwrap()[output.account], output); match tx_entry.notes.iter().find(|nd| nd.nullifier == new_note.nullifier) { None => tx_entry.notes.push(new_note), Some(_) => warn!("Tried to insert duplicate note for Tx {}", tx.txid) @@ -1021,8 +1060,8 @@ impl LightWallet { ); // TODO: This only spends from the first address right now. - let extsk = &self.extsks[0]; - let extfvk = &self.extfvks[0]; + let extsk = &self.extsks.read().unwrap()[0]; + let extfvk = &self.extfvks.read().unwrap()[0]; let ovk = extfvk.fvk.ovk; let to = match address::RecipientAddress::from_str(to, @@ -1082,7 +1121,7 @@ impl LightWallet { address::RecipientAddress::Shielded(_) => { // The destination is a sapling address, so add all transparent inputs // TODO: This only spends from the first address right now. - let sk = self.tkeys[0]; + let sk = self.tkeys.read().unwrap()[0]; // Add all tinputs tinputs.iter() @@ -1136,8 +1175,8 @@ impl LightWallet { // the builder will automatically send change to that address if notes.len() == 0 { builder.send_change_to( - ExtendedFullViewingKey::from(&self.extsks[0]).fvk.ovk, - self.extsks[0].default_address().unwrap().1); + ExtendedFullViewingKey::from(&self.extsks.read().unwrap()[0]).fvk.ovk, + self.extsks.read().unwrap()[0].default_address().unwrap().1); } // Compute memo if it exists @@ -1497,10 +1536,10 @@ pub mod tests { const AMOUNT1:u64 = 5; // Address is encoded in bech32 let address = Some(encode_payment_address(wallet.config.hrp_sapling_address(), - &wallet.extfvks[0].default_address().unwrap().1)); + &wallet.extfvks.read().unwrap()[0].default_address().unwrap().1)); let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); - cb1.add_tx_paying(wallet.extfvks[0].clone(), AMOUNT1); + cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); // Make sure that the intial state is empty assert_eq!(wallet.txs.read().unwrap().len(), 0); @@ -1519,7 +1558,7 @@ pub mod tests { // Add a second block let mut cb2 = FakeCompactBlock::new(1, cb1.hash()); - cb2.add_tx_paying(wallet.extfvks[0].clone(), AMOUNT2); + cb2.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT2); wallet.scan_block(&cb2.as_bytes()).unwrap(); @@ -1537,7 +1576,7 @@ pub mod tests { const AMOUNT1:u64 = 5; let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); - let (nf1, txid1) = cb1.add_tx_paying(wallet.extfvks[0].clone(), AMOUNT1); + let (nf1, txid1) = cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); wallet.scan_block(&cb1.as_bytes()).unwrap(); @@ -1551,7 +1590,7 @@ pub mod tests { let addr2 = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[0u8; 32])) .default_address().unwrap().1; let mut cb2 = FakeCompactBlock::new(1, cb1.hash()); - let txid2 = cb2.add_tx_spending((nf1, AMOUNT1), wallet.extfvks[0].clone(), addr2, AMOUNT2); + let txid2 = cb2.add_tx_spending((nf1, AMOUNT1), wallet.extfvks.read().unwrap()[0].clone(), addr2, AMOUNT2); wallet.scan_block(&cb2.as_bytes()).unwrap(); // Now, the original note should be spent and there should be a change @@ -1582,8 +1621,8 @@ pub mod tests { let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); - let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys[0]); - let taddr = wallet.address_from_sk(&wallet.tkeys[0]); + let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); + let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); const AMOUNT1: u64 = 20; @@ -1648,8 +1687,8 @@ pub mod tests { let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); - let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys[0]); - let taddr = wallet.address_from_sk(&wallet.tkeys[0]); + let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); + let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); let non_wallet_sk = &SecretKey::from_slice(&[1u8; 32]).unwrap(); let non_wallet_pk = PublicKey::from_secret_key(&secp, &non_wallet_sk); @@ -1721,7 +1760,7 @@ pub mod tests { const AMOUNT1:u64 = 5; let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); - let (nf1, txid1) = cb1.add_tx_paying(wallet.extfvks[0].clone(), AMOUNT1); + let (nf1, txid1) = cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); wallet.scan_block(&cb1.as_bytes()).unwrap(); @@ -1730,8 +1769,8 @@ pub mod tests { assert_eq!(wallet.zbalance(None), AMOUNT1); // Add a t input at the Tx - let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys[0]); - let taddr = wallet.address_from_sk(&wallet.tkeys[0]); + let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); + let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); const TAMOUNT1: u64 = 20; @@ -1745,7 +1784,7 @@ pub mod tests { let addr2 = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[0u8; 32])) .default_address().unwrap().1; let mut cb2 = FakeCompactBlock::new(1, cb1.hash()); - let txid2 = cb2.add_tx_spending((nf1, AMOUNT1), wallet.extfvks[0].clone(), addr2, AMOUNT2); + let txid2 = cb2.add_tx_spending((nf1, AMOUNT1), wallet.extfvks.read().unwrap()[0].clone(), addr2, AMOUNT2); wallet.scan_block(&cb2.as_bytes()).unwrap(); let mut tx = FakeTransaction::new_with_txid(txid2); @@ -1766,13 +1805,13 @@ pub mod tests { { assert_eq!(wallet.seed, wallet2.seed); - assert_eq!(wallet.extsks.len(), wallet2.extsks.len()); - assert_eq!(wallet.extsks[0], wallet2.extsks[0]); - assert_eq!(wallet.extfvks[0], wallet2.extfvks[0]); - assert_eq!(wallet.address[0], wallet2.address[0]); + 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.tkeys.len(), wallet2.tkeys.len()); - assert_eq!(wallet.tkeys[0], wallet2.tkeys[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]); } // Test blocks were serialized properly @@ -1836,7 +1875,7 @@ pub mod tests { let wallet = LightWallet::new(None, &config, 0).unwrap(); let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); - let (_, txid1) = cb1.add_tx_paying(wallet.extfvks[0].clone(), amount); + let (_, txid1) = cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), amount); wallet.scan_block(&cb1.as_bytes()).unwrap(); // We have one note @@ -1997,8 +2036,8 @@ pub mod tests { const AMOUNT_T: u64 = 40000; let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT_Z); - let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys[0]); - let taddr = wallet.address_from_sk(&wallet.tkeys[0]); + let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); + let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); let mut tx = FakeTransaction::new(&mut rng); tx.add_t_output(&pk, AMOUNT_T); @@ -2096,7 +2135,7 @@ pub mod tests { let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT1); let my_address = encode_payment_address(wallet.config.hrp_sapling_address(), - &wallet.extfvks[0].default_address().unwrap().1); + &wallet.extfvks.read().unwrap()[0].default_address().unwrap().1); let memo = "Incoming Memo".to_string(); let fee: u64 = DEFAULT_FEE.try_into().unwrap(); @@ -2123,7 +2162,7 @@ pub mod tests { assert_eq!(txs[&sent_txid].notes.len(), 1); - assert_eq!(txs[&sent_txid].notes[0].extfvk, wallet.extfvks[0]); + 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::memo_str(&txs[&sent_txid].notes[0].memo), Some(memo)); @@ -2136,7 +2175,7 @@ pub mod tests { const AMOUNT_SENT: u64 = 20000; let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT); - let taddr = wallet.address_from_sk(&wallet.tkeys[0]); + let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); let fee: u64 = DEFAULT_FEE.try_into().unwrap(); diff --git a/src/main.rs b/src/main.rs index 0661df7..a471e0f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ mod lightwallet; mod commands; use std::io::{Result, Error, ErrorKind}; -use std::sync::Arc; +use std::sync::{Arc}; use std::time::Duration; use lightclient::{LightClient, LightClientConfig}; @@ -136,7 +136,6 @@ pub fn main() { Err(e) => { eprintln!("Failed to start wallet. Error was:\n{}", e); return; } }; - // At startup, run a sync let sync_update = lightclient.do_sync(true); println!("{}", sync_update); @@ -150,7 +149,8 @@ pub fn main() { match command_rx.recv_timeout(Duration::from_secs(5 * 60)) { Ok((cmd, args)) => { let args = args.iter().map(|s| s.as_ref()).collect(); - let cmd_response = commands::do_user_command(&cmd, &args, &lc); + + let cmd_response = commands::do_user_command(&cmd, &args, lc.as_ref()); resp_tx.send(cmd_response).unwrap(); if cmd == "quit" { From 990026d621d83803487bbd9bc5106793789ab471 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Thu, 3 Oct 2019 11:41:31 -0700 Subject: [PATCH 2/8] Store exspk with the SpendableNote --- src/lightwallet.rs | 55 +++++++++++++++++++++-------------------- src/lightwallet/data.rs | 5 +++- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/src/lightwallet.rs b/src/lightwallet.rs index 089b6db..67386bb 100644 --- a/src/lightwallet.rs +++ b/src/lightwallet.rs @@ -597,20 +597,21 @@ impl LightWallet { None => { let address = self.address_from_pubkeyhash(vout.script_pubkey.address()); if address.is_none() { - println!("Couldn't determine address for output!"); + error!("Couldn't determine address for output!"); + } else { + info!("Added to wallet {}:{}", txid, n); + // Add the utxo + tx_entry.utxos.push(Utxo { + address: address.unwrap(), + txid: txid.clone(), + output_index: n, + script: vout.script_pubkey.0.clone(), + value: vout.value.into(), + height, + spent: None, + unconfirmed_spent: None, + }); } - info!("Added to wallet {}:{}", txid, n); - // Add the utxo - tx_entry.utxos.push(Utxo{ - address: address.unwrap(), - txid: txid.clone(), - output_index: n, - script: vout.script_pubkey.0.clone(), - value: vout.value.into(), - height, - spent: None, - unconfirmed_spent: None, - }); } } } @@ -622,10 +623,6 @@ impl LightWallet { // TODO: Save this object let secp = secp256k1::Secp256k1::new(); - // TODO: Iterate over all transparent addresses. This is currently looking only at - // the first one. - let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &self.tkeys.read().unwrap()[0]).serialize(); - let mut total_transparent_spend: u64 = 0; for vin in tx.vin.iter() { @@ -667,6 +664,9 @@ impl LightWallet { .total_transparent_value_spent = total_transparent_spend; } + // TODO: Iterate over all transparent addresses. This is currently looking only at + // the first one. + let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &self.tkeys.read().unwrap()[0]).serialize(); // Scan for t outputs for (n, vout) in tx.vout.iter().enumerate() { match vout.script_pubkey.address() { @@ -782,10 +782,10 @@ impl LightWallet { } // Update the WalletTx - info!("A sapling output was sent in {}", tx.txid()); + // Do it in a short scope because of the write lock. { - // Do it in a short scope because of the write lock. - + info!("A sapling output was sent in {}", tx.txid()); + let mut txs = self.txs.write().unwrap(); if txs.get(&tx.txid()).unwrap().outgoing_metadata.iter() .find(|om| om.address == address && om.value == note.value) @@ -1059,11 +1059,6 @@ impl LightWallet { to ); - // TODO: This only spends from the first address right now. - let extsk = &self.extsks.read().unwrap()[0]; - let extfvk = &self.extfvks.read().unwrap()[0]; - let ovk = extfvk.fvk.ovk; - let to = match address::RecipientAddress::from_str(to, self.config.hrp_sapling_address(), self.config.base58_pubkey_address(), @@ -1091,7 +1086,9 @@ impl LightWallet { let 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)) + .filter_map(|(txid, note)| + SpendableNote::from(txid, note, anchor_offset, &self.extsks.read().unwrap()[note.account]) + ) .scan(0, |running_total, spendable| { let value = spendable.note.value; let ret = if *running_total < u64::from(target_value) { @@ -1160,7 +1157,7 @@ impl LightWallet { for selected in notes.iter() { if let Err(e) = builder.add_sapling_spend( - extsk.clone(), + selected.extsk.clone(), selected.diversifier, selected.note.clone(), selected.witness.clone(), @@ -1183,6 +1180,10 @@ impl LightWallet { let encoded_memo = memo.map(|s| Memo::from_str(&s).unwrap() ); println!("{}: Adding output", now() - start_time); + + // TODO: We're using the first ovk to encrypt outgoing Txns. Is that Ok? + let ovk = self.extfvks.read().unwrap()[0].fvk.ovk; + if let Err(e) = match to { address::RecipientAddress::Shielded(to) => { builder.add_sapling_output(ovk, to.clone(), value, encoded_memo) diff --git a/src/lightwallet/data.rs b/src/lightwallet/data.rs index 2142e19..8ee5c80 100644 --- a/src/lightwallet/data.rs +++ b/src/lightwallet/data.rs @@ -22,6 +22,7 @@ use zcash_primitives::{ fs::{Fs, FsRepr}, } }; +use zcash_primitives::zip32::ExtendedSpendingKey; pub struct BlockData { @@ -468,10 +469,11 @@ pub struct SpendableNote { pub diversifier: Diversifier, pub note: Note, pub witness: IncrementalWitness, + pub extsk: ExtendedSpendingKey, } impl SpendableNote { - pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize) -> Option { + pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &ExtendedSpendingKey) -> Option { // Include only notes that haven't been spent, or haven't been included in an unconfirmed spend yet. if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1); @@ -482,6 +484,7 @@ impl SpendableNote { diversifier: nd.diversifier, note: nd.note.clone(), witness: w.clone(), + extsk: extsk.clone(), }) } else { None From 50356dbbbfccfd4ccbe681c8c9e364ceb68c850b Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Thu, 3 Oct 2019 21:14:12 -0700 Subject: [PATCH 3/8] Multi z and t addresses --- src/lightwallet.rs | 294 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 274 insertions(+), 20 deletions(-) diff --git a/src/lightwallet.rs b/src/lightwallet.rs index 67386bb..96dd81a 100644 --- a/src/lightwallet.rs +++ b/src/lightwallet.rs @@ -666,17 +666,23 @@ impl LightWallet { // TODO: Iterate over all transparent addresses. This is currently looking only at // the first one. - let pubkey = secp256k1::PublicKey::from_secret_key(&secp, &self.tkeys.read().unwrap()[0]).serialize(); // Scan for t outputs - for (n, vout) in tx.vout.iter().enumerate() { - match vout.script_pubkey.address() { - 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); - } - }, - _ => {} + let all_pubkeys = self.tkeys.read().unwrap().iter() + .map(|sk| + secp256k1::PublicKey::from_secret_key(&secp, sk).serialize() + ) + .collect::>(); + for pubkey in all_pubkeys { + for (n, vout) in tx.vout.iter().enumerate() { + match vout.script_pubkey.address() { + 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); + } + }, + _ => {} + } } } @@ -1109,16 +1115,20 @@ impl LightWallet { // Specifically, if you send an outgoing transaction that is sent to a shielded address, // ZecWallet will add all your t-address funds into that transaction, and send them to your shielded // address as change. - let tinputs = self.get_utxos().iter() - .filter(|utxo| utxo.unconfirmed_spent.is_none()) // Remove any unconfirmed spends - .map(|utxo| utxo.clone()) - .collect::>(); - + let mut tinputs = vec![]; + if let Err(e) = match to { address::RecipientAddress::Shielded(_) => { // The destination is a sapling address, so add all transparent inputs - // TODO: This only spends from the first address right now. - let sk = self.tkeys.read().unwrap()[0]; + tinputs.extend(self.get_utxos().iter() + .filter(|utxo| utxo.unconfirmed_spent.is_none()) // Remove any unconfirmed spends + .map(|utxo| utxo.clone())); + + // Create a map from address -> sk for all taddrs, so we can spend from the + // right address + let address_to_sk: HashMap<_, _> = self.tkeys.read().unwrap().iter().map(|sk| + (self.address_from_sk(&sk), sk.clone()) + ).collect(); // Add all tinputs tinputs.iter() @@ -1130,7 +1140,18 @@ impl LightWallet { script_pubkey: Script { 0: utxo.script.clone() }, }; - builder.add_transparent_input(sk, outpoint.clone(), coin.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); + eprintln!("{}", e); + + Err(zcash_primitives::transaction::builder::Error::InvalidAddress) + } + } + }) .collect::, _>>() }, @@ -1865,7 +1886,7 @@ pub mod tests { chain_name: "test".to_string(), sapling_activation_height: 0, consensus_branch_id: "000000".to_string(), - anchor_offset: 1 + anchor_offset: 0 } } @@ -1967,7 +1988,107 @@ pub mod tests { } } - #[test] + #[test] + fn test_multi_z() { + const AMOUNT1: u64 = 50000; + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); + + let zaddr2 = wallet.add_zaddr(); + + const AMOUNT_SENT: u64 = 20; + + let outgoing_memo = "Outgoing Memo".to_string(); + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) =get_sapling_params().unwrap(); + + // Create a tx and send to address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + &zaddr2, AMOUNT_SENT, Some(outgoing_memo.clone())).unwrap(); + + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid = sent_tx.txid(); + + 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); + + // 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 + // had both outputs in the same Tx) + let (change_note_number, ext_note_number) = { + let txs = wallet.txs.read().unwrap(); + if txs[&sent_txid].notes[0].is_change { (0,1) } else { (1,0) } + }; + + // Now this new Spent tx should be in, so the note should be marked confirmed spent + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + + // The sent tx should generate change + the new incoming note + assert_eq!(txs[&sent_txid].notes.len(), 2); + + assert_eq!(txs[&sent_txid].notes[change_note_number].note.value, AMOUNT1 - AMOUNT_SENT - fee); + assert_eq!(txs[&sent_txid].notes[change_note_number].account, 0); + assert_eq!(txs[&sent_txid].notes[change_note_number].is_change, true); + assert_eq!(txs[&sent_txid].notes[change_note_number].spent, None); + assert_eq!(txs[&sent_txid].notes[change_note_number].unconfirmed_spent, None); + assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[change_note_number].memo), None); + + assert_eq!(txs[&sent_txid].notes[ext_note_number].note.value, AMOUNT_SENT); + assert_eq!(txs[&sent_txid].notes[ext_note_number].account, 1); + assert_eq!(txs[&sent_txid].notes[ext_note_number].is_change, false); + assert_eq!(txs[&sent_txid].notes[ext_note_number].spent, None); + assert_eq!(txs[&sent_txid].notes[ext_note_number].unconfirmed_spent, None); + assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[ext_note_number].memo), Some(outgoing_memo)); + + assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); + + // No Outgoing meta data, since this is a wallet -> wallet tx + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 0); + } + + // Now spend the money, which should pick notes from both addresses + let amount_all:u64 = (AMOUNT1 - AMOUNT_SENT - fee) + (AMOUNT_SENT) - fee; + let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); + + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + &taddr, amount_all, None).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_ext_txid = sent_tx.txid(); + + 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); + + { + // Both notes should be spent now. + let txs = wallet.txs.read().unwrap(); + + assert_eq!(txs[&sent_txid].notes[change_note_number].is_change, true); + assert_eq!(txs[&sent_txid].notes[change_note_number].spent, Some(sent_ext_txid)); + assert_eq!(txs[&sent_txid].notes[change_note_number].unconfirmed_spent, None); + + assert_eq!(txs[&sent_txid].notes[ext_note_number].is_change, false); + assert_eq!(txs[&sent_txid].notes[ext_note_number].spent, Some(sent_ext_txid)); + assert_eq!(txs[&sent_txid].notes[ext_note_number].unconfirmed_spent, None); + + // Check outgoing metadata for the external sent tx + assert_eq!(txs[&sent_ext_txid].notes.len(), 0); // No change was generated + assert_eq!(txs[&sent_ext_txid].outgoing_metadata.len(), 1); + assert_eq!(txs[&sent_ext_txid].outgoing_metadata[0].address, taddr); + assert_eq!(txs[&sent_ext_txid].outgoing_metadata[0].value, amount_all); + } + } + + #[test] fn test_z_spend_to_taddr() { const AMOUNT1: u64 = 50000; let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); @@ -2227,6 +2348,139 @@ pub mod tests { } } + #[test] + fn test_multi_t() { + const AMOUNT: u64 = 5000000; + const AMOUNT_SENT1: u64 = 20000; + const AMOUNT_SENT2: u64 = 10000; + + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT); + + // Add a new taddr + let taddr2 = wallet.add_taddr(); + + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + // Create a Tx and send to the second t address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + &taddr2, AMOUNT_SENT1, None).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid1 = sent_tx.txid(); + + // Add it to a block + 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); + + // Check that the send to the second taddr worked + { + let txs = wallet.txs.read().unwrap(); + + // We have the original note + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT); + + // We have the spent tx + assert_eq!(txs[&sent_txid1].notes.len(), 1); + assert_eq!(txs[&sent_txid1].notes[0].note.value, AMOUNT - AMOUNT_SENT1 - fee); + assert_eq!(txs[&sent_txid1].notes[0].is_change, true); + assert_eq!(txs[&sent_txid1].notes[0].spent, None); + assert_eq!(txs[&sent_txid1].notes[0].unconfirmed_spent, None); + + // Since we sent the Tx to ourself, there should be no outgoing + // metadata + assert_eq!(txs[&sent_txid1].total_shielded_value_spent, AMOUNT); + assert_eq!(txs[&sent_txid1].outgoing_metadata.len(), 0); + + + // We have the taddr utxo in the same Tx + assert_eq!(txs[&sent_txid1].utxos.len(), 1); + assert_eq!(txs[&sent_txid1].utxos[0].address, taddr2); + assert_eq!(txs[&sent_txid1].utxos[0].value, AMOUNT_SENT1); + assert_eq!(txs[&sent_txid1].utxos[0].spent, None); + assert_eq!(txs[&sent_txid1].utxos[0].unconfirmed_spent, None); + } + + // Send some money to the 3rd t addr + let taddr3 = wallet.add_taddr(); + + // Create a Tx and send to the second t address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + &taddr3, AMOUNT_SENT2, None).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid2 = sent_tx.txid(); + + // Add it to a block + 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); + + // Quickly check we have it + { + let txs = wallet.txs.read().unwrap(); + + // We have the taddr utxo in the same Tx + assert_eq!(txs[&sent_txid2].utxos.len(), 1); + assert_eq!(txs[&sent_txid2].utxos[0].address, taddr3); + assert_eq!(txs[&sent_txid2].utxos[0].value, AMOUNT_SENT2); + + // Old UTXO was NOT spent here, because we sent it to a taddr + assert_eq!(txs[&sent_txid1].utxos.len(), 1); + assert_eq!(txs[&sent_txid1].utxos[0].value, AMOUNT_SENT1); + assert_eq!(txs[&sent_txid1].utxos[0].address, taddr2); + assert_eq!(txs[&sent_txid1].utxos[0].spent, None); + assert_eq!(txs[&sent_txid1].utxos[0].unconfirmed_spent, None); + } + + // Now, spend to an external z address, which will select all the utxos + let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32])); + let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(), + &fvk.default_address().unwrap().1); + + const AMOUNT_SENT_EXT: u64 = 45; + let outgoing_memo = "Outgoing Memo".to_string(); + + // Create a tx and send to address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + &ext_address, AMOUNT_SENT_EXT, Some(outgoing_memo.clone())).unwrap(); + + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid3 = sent_tx.txid(); + + 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); + + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&sent_txid3].outgoing_metadata.len(), 1); + + assert_eq!(txs[&sent_txid3].outgoing_metadata[0].address, ext_address); + assert_eq!(txs[&sent_txid3].outgoing_metadata[0].value, AMOUNT_SENT_EXT); + assert_eq!(txs[&sent_txid3].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); + + // Test to see both UTXOs were spent. + // UTXO1 + assert_eq!(txs[&sent_txid1].utxos[0].value, AMOUNT_SENT1); + assert_eq!(txs[&sent_txid1].utxos[0].address, taddr2); + assert_eq!(txs[&sent_txid1].utxos[0].spent, Some(sent_txid3)); + assert_eq!(txs[&sent_txid1].utxos[0].unconfirmed_spent, None); + + // UTXO2 + assert_eq!(txs[&sent_txid2].utxos[0].value, AMOUNT_SENT2); + assert_eq!(txs[&sent_txid2].utxos[0].address, taddr3); + assert_eq!(txs[&sent_txid2].utxos[0].spent, Some(sent_txid3)); + assert_eq!(txs[&sent_txid2].utxos[0].unconfirmed_spent, None); + } + + } + /// Test helper to add blocks fn add_blocks(wallet: &LightWallet, start: i32, num: i32, mut prev_hash: BlockHash) -> Result{ // Add it to a block From 5ca7319fc397f1acbd599b3c051f4e6cd33a1ca1 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Thu, 3 Oct 2019 21:51:37 -0700 Subject: [PATCH 4/8] Test multi addr serialization --- src/lightwallet.rs | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/lightwallet.rs b/src/lightwallet.rs index 96dd81a..854977d 100644 --- a/src/lightwallet.rs +++ b/src/lightwallet.rs @@ -1552,7 +1552,7 @@ pub mod tests { } #[test] - fn z_balances() { + fn test_z_balances() { let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); const AMOUNT1:u64 = 5; @@ -1591,7 +1591,7 @@ pub mod tests { } #[test] - fn z_change_balances() { + fn test_z_change_balances() { let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); // First, add an incoming transaction @@ -1880,6 +1880,38 @@ pub mod tests { } } + #[test] + fn test_multi_serialization() { + let config = get_test_config(); + + let wallet = LightWallet::new(None, &config, 0).unwrap(); + + let taddr1 = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); + let taddr2 = wallet.add_taddr(); + + let (zaddr1, zpk1) = &wallet.get_z_private_keys()[0]; + let zaddr2 = wallet.add_zaddr(); + + let mut serialized_data = vec![]; + wallet.write(&mut serialized_data).expect("Serialize wallet"); + let wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap(); + + 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!(taddr1, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0])); + assert_eq!(taddr2, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[1])); + + let (w2_zaddr1, w2_zpk1) = &wallet.get_z_private_keys()[0]; + let (w2_zaddr2, _) = &wallet.get_z_private_keys()[1]; + assert_eq!(zaddr1, w2_zaddr1); + assert_eq!(zpk1, w2_zpk1); + assert_eq!(zaddr2, *w2_zaddr2); + + } + fn get_test_config() -> LightClientConfig { LightClientConfig { server: "0.0.0.0:0".parse().unwrap(), From 94108e493641655f52313ff663b17a0a9d25cf0e Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 4 Oct 2019 10:10:51 -0700 Subject: [PATCH 5/8] Send to multiple addresses --- src/lightclient.rs | 2 +- src/lightwallet.rs | 182 ++++++++++++++++++++++++++------------------- 2 files changed, 105 insertions(+), 79 deletions(-) diff --git a/src/lightclient.rs b/src/lightclient.rs index dadf375..ff46c8d 100644 --- a/src/lightclient.rs +++ b/src/lightclient.rs @@ -709,7 +709,7 @@ impl LightClient { let rawtx = self.wallet.send_to_address( u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), // Blossom ID &self.sapling_spend, &self.sapling_output, - &addr, value, memo + vec![(&addr, value, memo)] ); match rawtx { diff --git a/src/lightwallet.rs b/src/lightwallet.rs index 854977d..dcfe717 100644 --- a/src/lightwallet.rs +++ b/src/lightwallet.rs @@ -1054,28 +1054,43 @@ impl LightWallet { consensus_branch_id: u32, spend_params: &[u8], output_params: &[u8], - to: &str, - value: u64, - memo: Option, + tos: Vec<(&str, u64, Option)> ) -> Option> { let start_time = now(); + + let total_value = tos.iter().map(|to| to.1).sum::(); + println!( - "0: Creating transaction sending {} tazoshis to {}", - value, - to + "0: Creating transaction sending {} tazoshis to {} addresses", + total_value, tos.len() ); - let to = match address::RecipientAddress::from_str(to, - self.config.hrp_sapling_address(), - self.config.base58_pubkey_address(), - self.config.base58_script_address()) { - Some(to) => to, - None => { - eprintln!("Invalid recipient address"); + // Convert address (str) to RecepientAddress and value to Amount + let maybe_tos: Result)>, _> = tos.iter().map(|to| { + let ra = match address::RecipientAddress::from_str(to.0, + self.config.hrp_sapling_address(), + self.config.base58_pubkey_address(), + self.config.base58_script_address()) { + Some(to) => to, + None => { + let e = format!("Invalid recipient address: {}", to.0); + error!("{}", e); + return Err(e); + } + }; + + let value = Amount::from_u64(to.1).unwrap(); + + Ok((ra, value, to.2.clone())) + }).collect(); + + let tos = match maybe_tos { + Ok(t) => t, + Err(e) => { + error!("{}", e); return None; } }; - let value = Amount::from_u64(value).unwrap(); // Target the next block, assuming we are up-to-date. let (height, anchor_offset) = match self.get_target_height_and_anchor_offset() { @@ -1088,7 +1103,7 @@ impl LightWallet { // Select notes to cover the target value println!("{}: Selecting notes", now() - start_time); - let target_value = value + DEFAULT_FEE ; + let target_value = Amount::from_u64(total_value).unwrap() + DEFAULT_FEE ; let notes: Vec<_> = self.txs.read().unwrap().iter() .map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note))) .flatten() @@ -1117,48 +1132,55 @@ impl LightWallet { // address as change. let mut tinputs = vec![]; - if let Err(e) = match to { - address::RecipientAddress::Shielded(_) => { - // The destination is a sapling address, so add all transparent inputs - tinputs.extend(self.get_utxos().iter() - .filter(|utxo| utxo.unconfirmed_spent.is_none()) // Remove any unconfirmed spends - .map(|utxo| utxo.clone())); - - // Create a map from address -> sk for all taddrs, so we can spend from the - // right address - let address_to_sk: HashMap<_, _> = self.tkeys.read().unwrap().iter().map(|sk| - (self.address_from_sk(&sk), sk.clone()) - ).collect(); + // Check if all to addresses are shielded + let all_shielded = !tos.iter().any(|to| match to.0 { + address::RecipientAddress::Transparent(_) => true, + _ => false + }); - // Add all tinputs - tinputs.iter() - .map(|utxo| { - let outpoint: OutPoint = utxo.to_outpoint(); - - let coin = TxOut { - value: Amount::from_u64(utxo.value).unwrap(), - script_pubkey: Script { 0: utxo.script.clone() }, - }; + if all_shielded { + // The destination is a sapling address, so add all transparent inputs + tinputs.extend(self.get_utxos().iter() + .filter(|utxo| utxo.unconfirmed_spent.is_none()) // Remove any unconfirmed spends + .map(|utxo| utxo.clone())); + + // Create a map from address -> sk for all taddrs, so we can spend from the + // right address + let address_to_sk: HashMap<_, _> = self.tkeys.read().unwrap().iter().map(|sk| + (self.address_from_sk(&sk), sk.clone()) + ).collect(); - 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); - eprintln!("{}", e); + // Add all tinputs + let r = tinputs.iter() + .map(|utxo| { + let outpoint: OutPoint = utxo.to_outpoint(); + + let coin = TxOut { + value: Amount::from_u64(utxo.value).unwrap(), + script_pubkey: Script { 0: utxo.script.clone() }, + }; - Err(zcash_primitives::transaction::builder::Error::InvalidAddress) - } + 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) } - - }) - .collect::, _>>() - }, - _ => Ok(vec![]) - } { - eprintln!("Error adding transparent inputs: {:?}", e); - return None; + } + + }) + .collect::, _>>(); + + match r { + Err(e) => { + error!("Error adding transparent inputs: {:?}", e); + return None; + }, + Ok(_) => {} + }; } // Confirm we were able to select sufficient value @@ -1197,25 +1219,29 @@ impl LightWallet { self.extsks.read().unwrap()[0].default_address().unwrap().1); } - // Compute memo if it exists - let encoded_memo = memo.map(|s| Memo::from_str(&s).unwrap() ); - - println!("{}: Adding output", now() - start_time); - // TODO: We're using the first ovk to encrypt outgoing Txns. Is that Ok? let ovk = self.extfvks.read().unwrap()[0].fvk.ovk; - if let Err(e) = match to { - address::RecipientAddress::Shielded(to) => { - builder.add_sapling_output(ovk, to.clone(), value, encoded_memo) + for (to, value, memo) in tos { + // Compute memo if it exists + let encoded_memo = memo.map(|s| Memo::from_str(&s).unwrap()); + + println!("{}: Adding output", now() - start_time); + + if let Err(e) = match to { + address::RecipientAddress::Shielded(to) => { + builder.add_sapling_output(ovk, to.clone(), value, encoded_memo) + } + address::RecipientAddress::Transparent(to) => { + builder.add_transparent_output(&to, value) + } + } { + eprintln!("Error adding output: {:?}", e); + return None; } - address::RecipientAddress::Transparent(to) => { - builder.add_transparent_output(&to, value) - } - } { - eprintln!("Error adding output: {:?}", e); - return None; } + + println!("{}: Building transaction", now() - start_time); let (tx, _) = match builder.build( consensus_branch_id, @@ -1969,7 +1995,7 @@ pub mod tests { // Create a tx and send to address let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - &ext_address, AMOUNT_SENT, Some(outgoing_memo.clone())).unwrap(); + vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -2037,7 +2063,7 @@ pub mod tests { // Create a tx and send to address let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - &zaddr2, AMOUNT_SENT, Some(outgoing_memo.clone())).unwrap(); + vec![(&zaddr2, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -2091,7 +2117,7 @@ pub mod tests { let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - &taddr, amount_all, None).unwrap(); + vec![(&taddr, amount_all, None)]).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_ext_txid = sent_tx.txid(); @@ -2133,7 +2159,7 @@ pub mod tests { let fee: u64 = DEFAULT_FEE.try_into().unwrap(); let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - &taddr, AMOUNT_SENT, None).unwrap(); + vec![(&taddr, AMOUNT_SENT, None)]).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -2225,7 +2251,7 @@ pub mod tests { // Create a tx and send to address. This should consume both the UTXO and the note let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - &ext_address, AMOUNT_SENT, Some(outgoing_memo.clone())).unwrap(); + vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -2299,7 +2325,7 @@ pub mod tests { // Create a tx and send to address let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - &my_address, AMOUNT1 - fee, Some(memo.clone())).unwrap(); + vec![(&my_address, AMOUNT1 - fee, Some(memo.clone()))]).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -2338,7 +2364,7 @@ pub mod tests { // Create a tx and send to address let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - &taddr, AMOUNT_SENT, None).unwrap(); + vec![(&taddr, AMOUNT_SENT, None)]).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -2398,7 +2424,7 @@ pub mod tests { // Create a Tx and send to the second t address let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - &taddr2, AMOUNT_SENT1, None).unwrap(); + vec![(&taddr2, AMOUNT_SENT1, None)]).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid1 = sent_tx.txid(); @@ -2442,7 +2468,7 @@ pub mod tests { // Create a Tx and send to the second t address let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - &taddr3, AMOUNT_SENT2, None).unwrap(); + vec![(&taddr3, AMOUNT_SENT2, None)]).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid2 = sent_tx.txid(); @@ -2479,7 +2505,7 @@ pub mod tests { // Create a tx and send to address let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - &ext_address, AMOUNT_SENT_EXT, Some(outgoing_memo.clone())).unwrap(); + vec![(&ext_address, AMOUNT_SENT_EXT, Some(outgoing_memo.clone()))]).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid3 = sent_tx.txid(); @@ -2606,7 +2632,7 @@ pub mod tests { const AMOUNT_SENT: u64 = 30000; let fee: u64 = DEFAULT_FEE.try_into().unwrap(); let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - &taddr, AMOUNT_SENT, None).unwrap(); + vec![(&taddr, AMOUNT_SENT, None)]).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); From ef9fcceb8c1aa62bdb27c8119ba63418190e542b Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 4 Oct 2019 21:18:01 -0700 Subject: [PATCH 6/8] Multi spend test --- src/lightwallet.rs | 137 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/src/lightwallet.rs b/src/lightwallet.rs index dcfe717..800f54a 100644 --- a/src/lightwallet.rs +++ b/src/lightwallet.rs @@ -2539,6 +2539,143 @@ pub mod tests { } + #[test] + fn test_multi_spends() { + const AMOUNT1: u64 = 50000; + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); + + let zaddr2 = wallet.add_zaddr(); + const ZAMOUNT2:u64 = 30; + let outgoing_memo2 = "Outgoing Memo2".to_string(); + + let zaddr3 = wallet.add_zaddr(); + const ZAMOUNT3:u64 = 40; + let outgoing_memo3 = "Outgoing Memo3".to_string(); + + let taddr2 = wallet.add_taddr(); + const TAMOUNT2:u64 = 50; + let taddr3 = wallet.add_taddr(); + const TAMOUNT3:u64 = 60; + + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + let tos = vec![ (zaddr2.as_str(), ZAMOUNT2, Some(outgoing_memo2.clone())), + (zaddr3.as_str(), ZAMOUNT3, Some(outgoing_memo3.clone())), + (taddr2.as_str(), TAMOUNT2, None), + (taddr3.as_str(), TAMOUNT3, None) ]; + + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, tos).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid = sent_tx.txid(); + + 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); + + // Make sure all the outputs are there! + { + let txs = wallet.txs.read().unwrap(); + + // The single note was spent + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + + // The outputs are all sent to the wallet, so they should + // correspond to notes & utxos. + // 2 notes + 1 change + assert_eq!(txs[&sent_txid].notes.len(), 3); + + // Find the change note + let change_note = txs[&sent_txid].notes.iter().find(|n| n.is_change).unwrap(); + assert_eq!(change_note.note.value, AMOUNT1 - (ZAMOUNT2+ZAMOUNT3+TAMOUNT2+TAMOUNT3+fee)); + assert_eq!(change_note.spent, None); + assert_eq!(change_note.unconfirmed_spent, None); + + // Find zaddr2 + let zaddr2_note = txs[&sent_txid].notes.iter().find(|n| n.note.value == ZAMOUNT2).unwrap(); + assert_eq!(zaddr2_note.account, 2-1); + assert_eq!(zaddr2_note.is_change, false); + assert_eq!(zaddr2_note.spent, None); + assert_eq!(zaddr2_note.unconfirmed_spent, None); + assert_eq!(LightWallet::memo_str(&zaddr2_note.memo), Some(outgoing_memo2)); + + // Find zaddr3 + let zaddr3_note = txs[&sent_txid].notes.iter().find(|n| n.note.value == ZAMOUNT3).unwrap(); + assert_eq!(zaddr3_note.account, 3-1); + assert_eq!(zaddr3_note.is_change, false); + assert_eq!(zaddr3_note.spent, None); + assert_eq!(zaddr3_note.unconfirmed_spent, None); + assert_eq!(LightWallet::memo_str(&zaddr3_note.memo), Some(outgoing_memo3)); + + // Find taddr2 + let utxo2 = txs[&sent_txid].utxos.iter().find(|u| u.value == TAMOUNT2).unwrap(); + assert_eq!(utxo2.address, taddr2); + assert_eq!(utxo2.txid, sent_txid); + assert_eq!(utxo2.spent, None); + assert_eq!(utxo2.unconfirmed_spent, None); + + // Find taddr3 + let utxo3 = txs[&sent_txid].utxos.iter().find(|u| u.value == TAMOUNT3).unwrap(); + assert_eq!(utxo3.address, taddr3); + assert_eq!(utxo3.txid, sent_txid); + assert_eq!(utxo3.spent, None); + assert_eq!(utxo3.unconfirmed_spent, None); + } + + // Now send an outgoing tx to one ext taddr and one ext zaddr + let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32])); + let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(), + &fvk.default_address().unwrap().1); + let ext_memo = "External memo".to_string(); + let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); + + const EXT_ZADDR_AMOUNT: u64 = 3000; + let ext_taddr_amount = AMOUNT1 - TAMOUNT2 - TAMOUNT3 - fee - EXT_ZADDR_AMOUNT - fee; // Spend everything + println!("taddr amount {}", ext_taddr_amount); + + let tos = vec![ (ext_address.as_str(), EXT_ZADDR_AMOUNT, Some(ext_memo.clone())), + (ext_taddr.as_str(), ext_taddr_amount, None)]; + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, tos).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid2 = sent_tx.txid(); + + 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); + + // Make sure all the outputs are there! + { + let txs = wallet.txs.read().unwrap(); + + // All notes were spent + assert_eq!(txs[&sent_txid].notes[0].spent, Some(sent_txid2)); + assert_eq!(txs[&sent_txid].notes[1].spent, Some(sent_txid2)); + assert_eq!(txs[&sent_txid].notes[2].spent, Some(sent_txid2)); + + // All utxos were NOT spent, since this outgoing Tx to a t addr + + // The new tx has no change + assert_eq!(txs[&sent_txid2].notes.len(), 0); + assert_eq!(txs[&sent_txid2].utxos.len(), 0); + + // Test the outgoing metadata + // Find the znote + let zoutgoing = txs[&sent_txid2].outgoing_metadata.iter().find(|o| o.address == ext_address).unwrap(); + assert_eq!(zoutgoing.value, EXT_ZADDR_AMOUNT); + assert_eq!(LightWallet::memo_str(&Some(zoutgoing.memo.clone())), Some(ext_memo)); + + // Find the taddr + let toutgoing = txs[&sent_txid2].outgoing_metadata.iter().find(|o| o.address == ext_taddr).unwrap(); + assert_eq!(toutgoing.value, ext_taddr_amount); + assert_eq!(LightWallet::memo_str(&Some(toutgoing.memo.clone())), None); + } + } + /// Test helper to add blocks fn add_blocks(wallet: &LightWallet, start: i32, num: i32, mut prev_hash: BlockHash) -> Result{ // Add it to a block From 28477bfedde635ce10c4218d2b477600e5733f5c Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 5 Oct 2019 09:44:20 -0700 Subject: [PATCH 7/8] Shield t address utxos even if sending to taddr --- src/lightwallet.rs | 102 ++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 57 deletions(-) diff --git a/src/lightwallet.rs b/src/lightwallet.rs index 800f54a..0b2980b 100644 --- a/src/lightwallet.rs +++ b/src/lightwallet.rs @@ -1130,58 +1130,49 @@ impl LightWallet { // Specifically, if you send an outgoing transaction that is sent to a shielded address, // ZecWallet will add all your t-address funds into that transaction, and send them to your shielded // address as change. - let mut tinputs = vec![]; + let tinputs: Vec<_> = self.get_utxos().iter() + .filter(|utxo| utxo.unconfirmed_spent.is_none()) // Remove any unconfirmed spends + .map(|utxo| utxo.clone()) + .collect(); - // Check if all to addresses are shielded - let all_shielded = !tos.iter().any(|to| match to.0 { - address::RecipientAddress::Transparent(_) => true, - _ => false - }); + // Create a map from address -> sk for all taddrs, so we can spend from the + // right address + let address_to_sk: HashMap<_, _> = self.tkeys.read().unwrap().iter().map(|sk| + (self.address_from_sk(&sk), sk.clone()) + ).collect(); - if all_shielded { - // The destination is a sapling address, so add all transparent inputs - tinputs.extend(self.get_utxos().iter() - .filter(|utxo| utxo.unconfirmed_spent.is_none()) // Remove any unconfirmed spends - .map(|utxo| utxo.clone())); - - // Create a map from address -> sk for all taddrs, so we can spend from the - // right address - let address_to_sk: HashMap<_, _> = self.tkeys.read().unwrap().iter().map(|sk| - (self.address_from_sk(&sk), sk.clone()) - ).collect(); + // Add all tinputs + let r = tinputs.iter() + .map(|utxo| { + let outpoint: OutPoint = utxo.to_outpoint(); + + let coin = TxOut { + value: Amount::from_u64(utxo.value).unwrap(), + script_pubkey: Script { 0: utxo.script.clone() }, + }; - // Add all tinputs - let r = tinputs.iter() - .map(|utxo| { - let outpoint: OutPoint = utxo.to_outpoint(); - - let coin = TxOut { - value: Amount::from_u64(utxo.value).unwrap(), - 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); - 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) - } + Err(zcash_primitives::transaction::builder::Error::InvalidAddress) } - - }) - .collect::, _>>(); - - match r { - Err(e) => { - error!("Error adding transparent inputs: {:?}", e); - return None; - }, - Ok(_) => {} - }; - } + } + + }) + .collect::, _>>(); + + match r { + Err(e) => { + error!("Error adding transparent inputs: {:?}", e); + return None; + }, + Ok(_) => {} + }; + // Confirm we were able to select sufficient value let selected_value = notes.iter().map(|selected| selected.note.value).sum::() @@ -2487,11 +2478,11 @@ pub mod tests { assert_eq!(txs[&sent_txid2].utxos[0].address, taddr3); assert_eq!(txs[&sent_txid2].utxos[0].value, AMOUNT_SENT2); - // Old UTXO was NOT spent here, because we sent it to a taddr + // Old UTXO was spent here assert_eq!(txs[&sent_txid1].utxos.len(), 1); assert_eq!(txs[&sent_txid1].utxos[0].value, AMOUNT_SENT1); assert_eq!(txs[&sent_txid1].utxos[0].address, taddr2); - assert_eq!(txs[&sent_txid1].utxos[0].spent, None); + assert_eq!(txs[&sent_txid1].utxos[0].spent, Some(sent_txid2)); assert_eq!(txs[&sent_txid1].utxos[0].unconfirmed_spent, None); } @@ -2523,12 +2514,7 @@ pub mod tests { assert_eq!(txs[&sent_txid3].outgoing_metadata[0].value, AMOUNT_SENT_EXT); assert_eq!(txs[&sent_txid3].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); - // Test to see both UTXOs were spent. - // UTXO1 - assert_eq!(txs[&sent_txid1].utxos[0].value, AMOUNT_SENT1); - assert_eq!(txs[&sent_txid1].utxos[0].address, taddr2); - assert_eq!(txs[&sent_txid1].utxos[0].spent, Some(sent_txid3)); - assert_eq!(txs[&sent_txid1].utxos[0].unconfirmed_spent, None); + // Test to see that the UTXOs were spent. // UTXO2 assert_eq!(txs[&sent_txid2].utxos[0].value, AMOUNT_SENT2); @@ -2634,7 +2620,7 @@ pub mod tests { let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); const EXT_ZADDR_AMOUNT: u64 = 3000; - let ext_taddr_amount = AMOUNT1 - TAMOUNT2 - TAMOUNT3 - fee - EXT_ZADDR_AMOUNT - fee; // Spend everything + let ext_taddr_amount = AMOUNT1 - fee - EXT_ZADDR_AMOUNT - fee; // Spend everything println!("taddr amount {}", ext_taddr_amount); let tos = vec![ (ext_address.as_str(), EXT_ZADDR_AMOUNT, Some(ext_memo.clone())), @@ -2657,7 +2643,9 @@ pub mod tests { assert_eq!(txs[&sent_txid].notes[1].spent, Some(sent_txid2)); assert_eq!(txs[&sent_txid].notes[2].spent, Some(sent_txid2)); - // All utxos were NOT spent, since this outgoing Tx to a t addr + // All utxos were spent + assert_eq!(txs[&sent_txid].utxos[0].spent, Some(sent_txid2)); + assert_eq!(txs[&sent_txid].utxos[1].spent, Some(sent_txid2)); // The new tx has no change assert_eq!(txs[&sent_txid2].notes.len(), 0); From b080c6061fd14175940bea0bc234d3205d5998e7 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Sat, 5 Oct 2019 15:25:12 -0700 Subject: [PATCH 8/8] Add bad spend tests --- src/lightclient.rs | 4 +- src/lightwallet.rs | 94 +++++++++++++++++++++++++++++----------------- 2 files changed, 61 insertions(+), 37 deletions(-) diff --git a/src/lightclient.rs b/src/lightclient.rs index ff46c8d..f71bf66 100644 --- a/src/lightclient.rs +++ b/src/lightclient.rs @@ -713,11 +713,11 @@ impl LightClient { ); match rawtx { - Some(txbytes) => match broadcast_raw_tx(&self.get_server_uri(), txbytes) { + Ok(txbytes) => match broadcast_raw_tx(&self.get_server_uri(), txbytes) { Ok(k) => k, Err(e) => e, }, - None => format!("No Tx to broadcast") + Err(e) => format!("No Tx to broadcast. Error was: {}", e) } } } diff --git a/src/lightwallet.rs b/src/lightwallet.rs index 0b2980b..901f882 100644 --- a/src/lightwallet.rs +++ b/src/lightwallet.rs @@ -1055,7 +1055,7 @@ impl LightWallet { spend_params: &[u8], output_params: &[u8], tos: Vec<(&str, u64, Option)> - ) -> Option> { + ) -> Result, String> { let start_time = now(); let total_value = tos.iter().map(|to| to.1).sum::(); @@ -1066,7 +1066,7 @@ impl LightWallet { ); // Convert address (str) to RecepientAddress and value to Amount - let maybe_tos: Result)>, _> = tos.iter().map(|to| { + let tos = tos.iter().map(|to| { let ra = match address::RecipientAddress::from_str(to.0, self.config.hrp_sapling_address(), self.config.base58_pubkey_address(), @@ -1082,22 +1082,15 @@ impl LightWallet { let value = Amount::from_u64(to.1).unwrap(); Ok((ra, value, to.2.clone())) - }).collect(); - - let tos = match maybe_tos { - Ok(t) => t, - Err(e) => { - error!("{}", e); - return None; - } - }; + }).collect::)>, String>>()?; // Target the next block, assuming we are up-to-date. let (height, anchor_offset) = match self.get_target_height_and_anchor_offset() { Some(res) => res, None => { - eprintln!("Cannot send funds before scanning any blocks"); - return None; + let e = format!("Cannot send funds before scanning any blocks"); + error!("{}", e); + return Err(e); } }; @@ -1142,7 +1135,7 @@ impl LightWallet { ).collect(); // Add all tinputs - let r = tinputs.iter() + tinputs.iter() .map(|utxo| { let outpoint: OutPoint = utxo.to_outpoint(); @@ -1163,15 +1156,8 @@ impl LightWallet { } }) - .collect::, _>>(); - - match r { - Err(e) => { - error!("Error adding transparent inputs: {:?}", e); - return None; - }, - Ok(_) => {} - }; + .collect::, _>>() + .map_err(|e| format!("{}", e))?; // Confirm we were able to select sufficient value @@ -1179,11 +1165,12 @@ impl LightWallet { + tinputs.iter().map::(|utxo| utxo.value.into()).sum::(); if selected_value < u64::from(target_value) { - eprintln!( + let e = format!( "Insufficient verified funds (have {}, need {:?}).\n Note, funds need {} confirmations before they can be spent", selected_value, target_value, self.config.anchor_offset ); - return None; + error!("{}", e); + return Err(e); } // Create the transaction @@ -1196,8 +1183,9 @@ impl LightWallet { selected.note.clone(), selected.witness.clone(), ) { - eprintln!("Error adding note: {:?}", e); - return None; + let e = format!("Error adding note: {:?}", e); + error!("{}", e); + return Err(e); } } @@ -1227,8 +1215,9 @@ impl LightWallet { builder.add_transparent_output(&to, value) } } { - eprintln!("Error adding output: {:?}", e); - return None; + let e = format!("Error adding output: {:?}", e); + error!("{}", e); + return Err(e); } } @@ -1240,8 +1229,9 @@ impl LightWallet { ) { Ok(res) => res, Err(e) => { - eprintln!("Error creating transaction: {:?}", e); - return None; + let e = format!("Error creating transaction: {:?}", e); + error!("{}", e); + return Err(e); } }; println!("{}: Transaction created", now() - start_time); @@ -1271,7 +1261,7 @@ impl LightWallet { // Return the encoded transaction, so the caller can send it. let mut raw_tx = vec![]; tx.write(&mut raw_tx).unwrap(); - Some(raw_tx.into_boxed_slice()) + Ok(raw_tx.into_boxed_slice()) } } @@ -2143,7 +2133,7 @@ pub mod tests { let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) =get_sapling_params().unwrap(); + let (ss, so) = get_sapling_params().unwrap(); let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); const AMOUNT_SENT: u64 = 30; @@ -2312,7 +2302,7 @@ pub mod tests { let fee: u64 = DEFAULT_FEE.try_into().unwrap(); let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) =get_sapling_params().unwrap(); + let (ss, so) = get_sapling_params().unwrap(); // Create a tx and send to address let raw_tx = wallet.send_to_address(branch_id, &ss, &so, @@ -2664,6 +2654,41 @@ pub mod tests { } } + #[test] + fn test_bad_send() { + // Test all the ways in which a send should fail + const AMOUNT1: u64 = 50000; + let _fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let (wallet, _txid1, _block_hash) = get_test_wallet(AMOUNT1); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); + + // Bad address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&"badaddress", 10, None)]); + assert!(raw_tx.err().unwrap().contains("Invalid recipient address")); + + // Insufficient funds + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_taddr, AMOUNT1 + 10, None)]); + assert!(raw_tx.err().unwrap().contains("Insufficient verified funds")); + } + + #[test] + #[should_panic] + fn test_bad_params() { + let (wallet, _, _) = get_test_wallet(100000); + let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + // Bad params + let _ = wallet.send_to_address(branch_id, &[], &[], + vec![(&ext_taddr, 10, None)]); + } + /// Test helper to add blocks fn add_blocks(wallet: &LightWallet, start: i32, num: i32, mut prev_hash: BlockHash) -> Result{ // Add it to a block @@ -2677,7 +2702,6 @@ pub mod tests { }; } - Ok(new_blk.hash()) }