From 2dce6b46ca3fb8f26a11826feb191567ac33f67e Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Wed, 8 Jul 2020 10:25:31 -0700 Subject: [PATCH 01/21] Add memos in outgoing_metadata even if change. Fixes #28 --- lib/src/lightwallet.rs | 18 +++- lib/src/lightwallet/tests.rs | 177 ++++++++++++++++++++++++++++++++++- 2 files changed, 187 insertions(+), 8 deletions(-) diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index d2244d0..69a1c78 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -1090,8 +1090,8 @@ impl LightWallet { None => continue, }; - { - info!("A sapling note was sent in {}, getting memo", tx.txid()); + if memo.to_utf8().is_some() { + info!("A sapling note was sent to wallet in {} that had a memo", tx.txid()); // Do it in a short scope because of the write lock. let mut txs = self.txs.write().unwrap(); @@ -1101,7 +1101,10 @@ impl LightWallet { .and_then(|t| { t.notes.iter_mut().find(|nd| nd.note == note) }) { - None => (), + None => { + info!("No txid matched for incoming sapling funds while updating memo"); + () + }, Some(nd) => { nd.memo = Some(memo) } @@ -1135,8 +1138,13 @@ impl LightWallet { let address = encode_payment_address(self.config.hrp_sapling_address(), &payment_address); - // Check if this is a change address - if z_addresses.contains(&address) { + // Check if this is change, and if it also doesn't have a memo, don't add + // to the outgoing metadata. + // If this is change (i.e., funds sent to ourself) AND has a memo, then + // presumably the users is writing a memo to themself, so we will add it to + // the outgoing metadata, even though it might be confusing in the UI, but hopefully + // the user can make sense of it. + if z_addresses.contains(&address) && memo.to_utf8().is_none() { continue; } diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index 7c5b318..738ba72 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -790,6 +790,176 @@ fn test_z_spend_to_z() { } } + +#[test] +fn test_self_txns_ttoz_withmemo() { + let mut rng = OsRng; + let secp = Secp256k1::new(); + + let (wallet, _txid1, block_hash) = get_test_wallet(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 = 50000; + + let mut tx = FakeTransaction::new(&mut rng); + tx.add_t_output(&pk, TAMOUNT1); + let txid1 = tx.get_tx().txid(); + + wallet.scan_full_tx(&tx.get_tx(), 1, 0); + + { + let txs = wallet.txs.read().unwrap(); + + // Now make sure the t addr was recieved + assert_eq!(txs[&txid1].utxos.len(), 1); + assert_eq!(txs[&txid1].utxos[0].address, taddr); + assert_eq!(txs[&txid1].utxos[0].value, TAMOUNT1); + } + + // Create a new Tx, spending this taddr + const AMOUNT_SENT: u64 = 20; + + let outgoing_memo = "Outgoing Memo".to_string(); + let zaddr = wallet.add_zaddr(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) =get_sapling_params().unwrap(); + + // 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, + vec![(&zaddr, 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); + + // Scan the compact block and the full Tx + wallet.scan_block(&cb3.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx, 2, 0); + + { + let txs = wallet.txs.read().unwrap(); + + // Includes Outgoing meta data, since this is a wallet -> wallet tx with a memo + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); + } +} + +#[test] +fn test_self_txns_ttoz_nomemo() { + let mut rng = OsRng; + let secp = Secp256k1::new(); + + let (wallet, _txid1, block_hash) = get_test_wallet(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 = 50000; + + let mut tx = FakeTransaction::new(&mut rng); + tx.add_t_output(&pk, TAMOUNT1); + let txid1 = tx.get_tx().txid(); + + wallet.scan_full_tx(&tx.get_tx(), 1, 0); + + { + let txs = wallet.txs.read().unwrap(); + + // Now make sure the t addr was recieved + assert_eq!(txs[&txid1].utxos.len(), 1); + assert_eq!(txs[&txid1].utxos[0].address, taddr); + assert_eq!(txs[&txid1].utxos[0].value, TAMOUNT1); + } + + // Create a new Tx, spending this taddr + const AMOUNT_SENT: u64 = 20; + + let zaddr = wallet.add_zaddr(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) =get_sapling_params().unwrap(); + + // 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, + vec![(&zaddr, AMOUNT_SENT, None)]).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); + + // Scan the compact block and the full Tx + wallet.scan_block(&cb3.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx, 2, 0); + + { + let txs = wallet.txs.read().unwrap(); + + // No Outgoing meta data, since this is a wallet -> wallet tx without a memo + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 0); + } +} + +#[test] +fn test_self_txns_ztoz() { + const AMOUNT1: u64 = 50000; + let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT1); + + let zaddr2 = wallet.add_zaddr(); // This is acually address #6, since there are 5 initial addresses in the wallet + + const AMOUNT_SENT: u64 = 20; + + let outgoing_memo = "Outgoing Memo".to_string(); + + 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, + vec![(&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, 0); + + { + let txs = wallet.txs.read().unwrap(); + + // Includes Outgoing meta data, since this is a wallet -> wallet tx with a memo + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); + } + + // Another self tx, this time without a memo + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&zaddr2, AMOUNT_SENT, None)]).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_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, 0); + + { + let txs = wallet.txs.read().unwrap(); + + // No Outgoing meta data, since this is a wallet -> wallet tx without a memo + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 0); + } +} + #[test] fn test_multi_z() { const AMOUNT1: u64 = 50000; @@ -848,12 +1018,13 @@ fn test_multi_z() { 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!(LightWallet::memo_str(&txs[&sent_txid].notes[ext_note_number].memo), Some(outgoing_memo.clone())); 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); + // Includes Outgoing meta data, since this is a wallet -> wallet tx with a memo + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); } // Now spend the money, which should pick notes from both addresses From f0e13e3cafb2bb1e77c27f6c7dff445449d71d0b Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 10 Jul 2020 10:08:52 -0700 Subject: [PATCH 02/21] Allow raw memos to be fetched. Fixes #30 --- lib/src/commands.rs | 21 ++++++++++++++++++--- lib/src/lightclient.rs | 31 ++++++++++++++++++++++++------- lib/src/lightwallet.rs | 36 +++++++++++++++++------------------- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/lib/src/commands.rs b/lib/src/commands.rs index 9a248d3..c63dc16 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -633,8 +633,9 @@ impl Command for TransactionsCommand { let mut h = vec![]; h.push("List all incoming and outgoing transactions from this wallet"); h.push("Usage:"); - h.push("list"); + h.push("list [allmemos]"); h.push(""); + h.push("If you include the 'allmemos' argument, all memos are returned in their raw hex format"); h.join("\n") } @@ -643,8 +644,22 @@ impl Command for TransactionsCommand { "List all transactions in the wallet".to_string() } - fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { - format!("{}", lightclient.do_list_transactions().pretty(2)) + fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { + if args.len() > 1 { + return format!("Didn't understand arguments\n{}", self.help()); + } + + let include_memo_hex = if args.len() == 1 { + if args[0] == "allmemos" || args[0] == "true" || args[0] == "yes" { + true + } else { + return format!("Couldn't understand first argument '{}'\n{}", args[0], self.help()); + } + } else { + false + }; + + format!("{}", lightclient.do_list_transactions(include_memo_hex).pretty(2)) } } diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 35819c7..c1a157b 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -862,7 +862,7 @@ impl LightClient { } } - pub fn do_list_transactions(&self) -> JsonValue { + pub fn do_list_transactions(&self, include_memo_hex: bool) -> JsonValue { let wallet = self.wallet.read().unwrap(); // Create a list of TransactionItems from wallet txns @@ -882,11 +882,18 @@ impl LightClient { // Collect outgoing metadata let outgoing_json = v.outgoing_metadata.iter() - .map(|om| - object!{ + .map(|om| { + let mut o = object!{ "address" => om.address.clone(), "value" => om.value, - "memo" => LightWallet::memo_str(&Some(om.memo.clone())), + "memo" => LightWallet::memo_str(&Some(om.memo.clone())) + }; + + if include_memo_hex { + o.insert("memohex", hex::encode(om.memo.as_bytes())).unwrap(); + } + + return o; }) .collect::>(); @@ -905,15 +912,25 @@ impl LightClient { txns.extend(v.notes.iter() .filter( |nd| !nd.is_change ) .enumerate() - .map ( |(i, nd)| - object! { + .map ( |(i, nd)| { + let mut o = object! { "block_height" => v.block, "datetime" => v.datetime, "position" => i, "txid" => format!("{}", v.txid), "amount" => nd.note.value as i64, "address" => LightWallet::note_address(self.config.hrp_sapling_address(), nd), - "memo" => LightWallet::memo_str(&nd.memo), + "memo" => LightWallet::memo_str(&nd.memo) + }; + + if include_memo_hex { + o.insert("memohex", match &nd.memo { + Some(m) => hex::encode(m.as_bytes()), + _ => "".to_string(), + }).unwrap(); + } + + return o; }) ); diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 69a1c78..5e6a7d5 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -1089,27 +1089,25 @@ impl LightWallet { Some(ret) => ret, None => continue, }; + + info!("A sapling note was sent to wallet in {}", tx.txid()); + + // Do it in a short scope because of the write lock. + let mut txs = self.txs.write().unwrap(); - if memo.to_utf8().is_some() { - info!("A sapling note was sent to wallet in {} that had a memo", tx.txid()); - - // Do it in a short scope because of the write lock. - let mut txs = self.txs.write().unwrap(); - - // Update memo if we have this Tx. - match txs.get_mut(&tx.txid()) - .and_then(|t| { - t.notes.iter_mut().find(|nd| nd.note == note) - }) { - None => { - info!("No txid matched for incoming sapling funds while updating memo"); - () - }, - Some(nd) => { - nd.memo = Some(memo) - } + // Update memo if we have this Tx. + match txs.get_mut(&tx.txid()) + .and_then(|t| { + t.notes.iter_mut().find(|nd| nd.note == note) + }) { + None => { + info!("No txid matched for incoming sapling funds while updating memo"); + () + }, + Some(nd) => { + nd.memo = Some(memo) } - } + } } // Also scan the output to see if it can be decoded with our OutgoingViewKey From 4c87e55afa9ee48082a9aeb47be528f8df6be07d Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Tue, 21 Jul 2020 10:12:09 -0700 Subject: [PATCH 03/21] hex memos --- lib/src/lightwallet.rs | 27 +++++++++++++++++------ lib/src/lightwallet/tests.rs | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 5e6a7d5..5404ee7 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -1842,13 +1842,26 @@ impl LightWallet { // Compute memo if it exists let encoded_memo = match memo { None => None, - Some(s) => match Memo::from_bytes(s.as_bytes()) { - None => { - let e = format!("Error creating output. Memo {:?} is too long", s); - error!("{}", e); - return Err(e); - }, - Some(m) => Some(m) + Some(s) => { + // If the string starts with an "0x", and contains only hex chars ([a-f0-9]+) then + // interpret it as a hex + let s_bytes = if s.to_lowercase().starts_with("0x") { + match hex::decode(&s[2..s.len()]) { + Ok(data) => data, + Err(_) => Vec::from(s.as_bytes()) + } + } else { + Vec::from(s.as_bytes()) + }; + + match Memo::from_bytes(&s_bytes) { + None => { + let e = format!("Error creating output. Memo {:?} is too long", s); + error!("{}", e); + return Err(e); + }, + Some(m) => Some(m) + } } }; diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index 738ba72..40b5fa8 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -1295,6 +1295,48 @@ fn test_z_incoming_memo() { } } + +#[test] +fn test_z_incoming_hex_memo() { + const AMOUNT1: u64 = 50000; + let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT1); + + let my_address = encode_payment_address(wallet.config.hrp_sapling_address(), + &wallet.extfvks.read().unwrap()[0].default_address().unwrap().1); + + let orig_memo = "hello world".to_string(); + let memo = format!("0x{}", hex::encode(&orig_memo)); + 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, + vec![(&my_address, AMOUNT1 - fee, Some(memo.clone()))]).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid = 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(); + + // And scan the Full Tx to get the memo + wallet.scan_full_tx(&sent_tx, 2, 0); + + { + let txs = wallet.txs.read().unwrap(); + + assert_eq!(txs[&sent_txid].notes.len(), 1); + + 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!(LightWallet::note_address(wallet.config.hrp_sapling_address(), &txs[&sent_txid].notes[0]), Some(my_address)); + assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[0].memo), Some(orig_memo)); + } +} + #[test] fn test_add_new_zt_hd_after_incoming() { // When an address recieves funds, a new, unused address should automatically get added From fb1135328ff2a6149fc26f61debdd76a0d5193bd Mon Sep 17 00:00:00 2001 From: adityapk00 <31996805+adityapk00@users.noreply.github.com> Date: Tue, 21 Jul 2020 12:08:04 -0700 Subject: [PATCH 04/21] Viewing Keys (#32) Add support for importing viewing keys and upgrading to spending keys. --- cli/src/version.rs | 2 +- lib/Cargo.toml | 2 +- lib/src/commands.rs | 99 ++++--- lib/src/lightclient.rs | 170 ++++++++++-- lib/src/lightwallet.rs | 391 +++++++++++++++++++-------- lib/src/lightwallet/bugs.rs | 130 --------- lib/src/lightwallet/data.rs | 11 +- lib/src/lightwallet/tests.rs | 271 ++++++++++++++++--- lib/src/lightwallet/walletzkey.rs | 421 ++++++++++++++++++++++++++++++ 9 files changed, 1149 insertions(+), 348 deletions(-) delete mode 100644 lib/src/lightwallet/bugs.rs create mode 100644 lib/src/lightwallet/walletzkey.rs diff --git a/cli/src/version.rs b/cli/src/version.rs index dc422bc..f6cbe38 100644 --- a/cli/src/version.rs +++ b/cli/src/version.rs @@ -1 +1 @@ -pub const VERSION:&str = "1.3.3"; +pub const VERSION:&str = "1.3.3"; \ No newline at end of file diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 14252c5..ac23d74 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -29,7 +29,7 @@ ring = "0.16.9" libflate = "0.1" subtle = "2" threadpool = "1.8.0" -num_cpus = "1.13.0" +num_cpus = "1.12.0" tonic = { version = "0.2.1", features = ["tls", "tls-roots"] } bytes = "0.4" diff --git a/lib/src/commands.rs b/lib/src/commands.rs index c63dc16..fadd7f2 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -308,16 +308,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) { @@ -663,13 +653,69 @@ 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 [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() < 2 || args.len() > 3 { + return format!("Insufficient arguments\n\n{}", self.help()); + } + + let key = args[0]; + let birthday = match args[1].parse::() { + Ok(b) => b, + Err(_) => return format!("Couldn't parse {} as birthday. Please specify an integer. Ok to use '0'", args[1]), + }; + + 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(), birthday) { + 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 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."); @@ -680,11 +726,7 @@ 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()); - } - + fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String { format!("{}", object! { "height" => lightclient.last_scanned_height()}.pretty(2)) } } @@ -757,29 +799,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 { @@ -816,6 +835,7 @@ pub fn get_commands() -> Box>> { 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("export".to_string(), Box::new(ExportCommand{})); map.insert("info".to_string(), Box::new(InfoCommand{})); map.insert("send".to_string(), Box::new(SendCommand{})); @@ -829,7 +849,6 @@ pub fn get_commands() -> Box>> { 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) } diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index c1a157b..570259e 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -18,9 +18,7 @@ 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; @@ -175,6 +173,24 @@ impl LightClientConfig { zcash_data_location.into_boxed_path() } + pub fn get_zcash_params_path(&self) -> io::Result> { + 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_wallet_path(&self) -> Box { let mut wallet_location = self.get_zcash_data_path().into_path_buf(); wallet_location.push(WALLET_NAME); @@ -253,6 +269,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; 2] { match &self.chain_name[..] { "main" => mainnet::B58_PUBKEY_ADDRESS_PREFIX, @@ -308,6 +333,17 @@ impl LightClient { }; } + fn write_file_if_not_exists(dir: &Box, 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 @@ -339,6 +375,25 @@ impl LightClient { 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(()) } @@ -477,12 +532,6 @@ impl LightClient { info!("Read wallet with birthday {}", lc.wallet.read().unwrap().get_first_tx_block()); info!("Created LightClient to {}", &config.server); - if crate::lightwallet::bugs::BugBip39Derivation::has_bug(&lc) { - let m = format!("WARNING!!!\nYour wallet has a bip39derivation bug that's showing incorrect addresses.\nPlease run 'fixbip39bug' to automatically fix the address derivation in your wallet!\nPlease see: https://github.com/adityapk00/zecwallet-light-cli/blob/master/bip39bug.md"); - info!("{}", m); - println!("{}", m); - } - Ok(lc) } @@ -577,11 +626,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::>(); @@ -609,9 +659,7 @@ 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::>(); + let z_addresses = wallet.get_all_zaddresses(); // Collect t addresses let t_addresses = wallet.taddresses.read().unwrap().iter().map( |a| a.clone() ) @@ -627,12 +675,12 @@ 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::>(); @@ -650,6 +698,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, @@ -999,7 +1048,7 @@ impl LightClient { let new_address = { let wallet = self.wallet.write().unwrap(); - match addr_type { + let addr = match addr_type { "z" => wallet.add_zaddr(), "t" => wallet.add_taddr(), _ => { @@ -1007,7 +1056,15 @@ 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()?; @@ -1015,6 +1072,69 @@ impl LightClient { 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 { + 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 { + 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 { + 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()?; + + Ok(array![new_address]) + } + + pub fn clear_state(&self) { // First, clear the state from the wallet self.wallet.read().unwrap().clear_blocks(); @@ -1411,6 +1531,14 @@ pub mod tests { assert!(!lc.do_new_address("z").is_err()); } + #[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_addresses() { let lc = super::LightClient::unconnected(TEST_SEED.to_string(), None).unwrap(); diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 5404ee7..a3b0459 100644 --- a/lib/src/lightwallet.rs +++ b/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} }; @@ -55,10 +57,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, WalletZKeyType}; pub const MAX_REORG: usize = 100; pub const GAP_RULE_UNUSED_ADDRESSES: usize = 5; @@ -112,13 +115,10 @@ 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>>, - extfvks: Arc>>, + // 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>>, - pub zaddress: Arc>>>, - // Transparent keys. If the wallet is locked, then the secret keys will be encrypted, // but the addresses will be present. tkeys: Arc>>, @@ -143,7 +143,7 @@ pub struct LightWallet { impl LightWallet { pub fn serialized_version() -> u64 { - return 6; + return 7; } fn get_taddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> SecretKey { @@ -219,8 +219,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, @@ -228,9 +229,7 @@ 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])), + zkeys: Arc::new(RwLock::new(vec![WalletZKey::new_hdkey(hdkey_num, extsk)])), tkeys: Arc::new(RwLock::new(vec![tpk])), taddresses: Arc::new(RwLock::new(vec![taddr])), blocks: Arc::new(RwLock::new(vec![])), @@ -294,22 +293,59 @@ 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 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::>() + 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::>() + }; + + // Calculate the addresses + let addresses = extfvks.iter().map( |fvk| fvk.default_address().unwrap().1 ) + .collect::>>(); + + // 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::>>() + } 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::>>() + }; + + // 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::>()? + } 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::>>(); - let tkeys = Vector::read(&mut reader, |r| { let mut tpk_bytes = [0u8; 32]; r.read_exact(&mut tpk_bytes)?; @@ -349,9 +385,7 @@ 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)), + zkeys: Arc::new(RwLock::new(zkeys)), tkeys: Arc::new(RwLock::new(tkeys)), taddresses: Arc::new(RwLock::new(taddresses)), blocks: Arc::new(RwLock::new(blocks)), @@ -387,14 +421,9 @@ 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 @@ -458,14 +487,20 @@ 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::>() + // 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::>(); + + keys } /// Get all t-address private keys. Returns a Vector of (address, secretkey) @@ -481,29 +516,34 @@ 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; + // 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 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(); } let pos = self.tkeys.read().unwrap().len() as u32; @@ -518,6 +558,81 @@ impl LightWallet { 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 @@ -623,6 +738,12 @@ impl LightWallet { } } + pub fn get_all_zaddresses(&self) -> Vec { + self.zkeys.read().unwrap().iter().map( |zk| { + encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress) + }).collect() + } + pub fn address_from_prefix_sk(prefix: &[u8; 2], sk: &secp256k1::SecretKey) -> String { let secp = secp256k1::Secp256k1::new(); let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk); @@ -661,8 +782,6 @@ 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")); } @@ -674,8 +793,12 @@ 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.zkeys.write().unwrap().iter_mut() + .map(|k| k.encrypt(&key)) + .collect::>>()?; self.encrypted = true; self.lock()?; @@ -695,16 +818,18 @@ 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 zkeys + self.zkeys.write().unwrap().iter_mut().map(|zk| { + zk.lock() + }).collect::>>()?; + self.unlocked = false; Ok(()) } pub fn unlock(&mut self, passwd: String) -> io::Result<()> { - use sodiumoxide::crypto::secretbox; - if !self.encrypted { return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is not encrypted")); } @@ -729,26 +854,6 @@ 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() { @@ -763,8 +868,12 @@ impl LightWallet { 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::>>()?; + // 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); @@ -785,7 +894,12 @@ impl LightWallet { if !self.unlocked { self.unlock(passwd)?; } - + + // Remove encryption from individual zkeys + self.zkeys.write().unwrap().iter_mut().map(|zk| { + zk.remove_encryption() + }).collect::>>()?; + // Permanantly remove the encryption self.encrypted = false; self.nonce = vec![]; @@ -861,6 +975,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( @@ -871,7 +986,48 @@ 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::() + } else { + 0 + } + }) + .sum::() + } + + pub fn spendable_zbalance(&self, addr: Option) -> u64 { + let anchor_height = match self.get_target_height_and_anchor_offset() { + Some((height, anchor_offset)) => height - anchor_offset as u32 - 1, + None => return 0, + }; + + 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. + match self.zkeys.read().unwrap().iter().find(|zk| zk.extfvk == nd.extfvk) { + Some(zk) => zk.keytype == WalletZKeyType::HdKey || zk.keytype == WalletZKeyType::ImportedSpendingKey, + _ => false + } + }) + .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 + .to_payment_address(nd.diversifier, &JUBJUB).unwrap() + ), + None => true + } + }) + .map(|nd| nd.note.value) .sum::() } else { 0 @@ -945,8 +1101,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::>() }; @@ -1074,18 +1233,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, }; @@ -1116,17 +1275,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::>(); // 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(), @@ -1561,17 +1721,19 @@ impl LightWallet { new_txs = { let nf_refs = nfs.iter().map(|(nf, account, _)| (nf.to_vec(), *account)).collect::>(); + let extfvks: Vec = 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[..], @@ -1634,7 +1796,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); @@ -1737,9 +1899,18 @@ 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, &self.extsks.read().unwrap()[note.account]) - ) + .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) { @@ -1831,12 +2002,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 diff --git a/lib/src/lightwallet/bugs.rs b/lib/src/lightwallet/bugs.rs deleted file mode 100644 index 8a88170..0000000 --- a/lib/src/lightwallet/bugs.rs +++ /dev/null @@ -1,130 +0,0 @@ -/// -/// In v1.0 of zecwallet-cli, there was a bug that incorrectly derived HD wallet keys after the first key. That is, the -/// first key, address was correct, but subsequent ones were not. -/// -/// The issue was that the 32-byte seed was directly being used to derive then subsequent addresses instead of the -/// 64-byte pkdf2(seed). The issue affected both t and z addresses -/// -/// To fix the bug, we need to: -/// 1. Check if the wallet has more than 1 address for t or z addresses -/// 2. Move any funds in these addresses to the first address -/// 3. Re-derive the addresses - -use super::LightWallet; -use crate::lightclient::LightClient; - -use json::object; -use bip39::{Mnemonic, Language}; - -pub struct BugBip39Derivation {} - -impl BugBip39Derivation { - - /// Check if this bug exists in the wallet - pub fn has_bug(client: &LightClient) -> bool { - let wallet = client.wallet.read().unwrap(); - - if wallet.zaddress.read().unwrap().len() <= 1 { - return false; - } - - 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); - } -} \ No newline at end of file diff --git a/lib/src/lightwallet/data.rs b/lib/src/lightwallet/data.rs index 1acc84e..5354e52 100644 --- a/lib/src/lightwallet/data.rs +++ b/lib/src/lightwallet/data.rs @@ -140,11 +140,10 @@ impl SaplingNoteData { // Reading a note also needs the corresponding address to read from. pub fn read(mut reader: R) -> io::Result { - let version = reader.read_u64::()?; - assert_eq!(version, SaplingNoteData::serialized_version()); + let _version = reader.read_u64::()?; let account = reader.read_u64::()? as usize; - + let extfvk = ExtendedFullViewingKey::read(&mut reader)?; let mut diversifier_bytes = [0u8; 11]; @@ -485,9 +484,9 @@ pub struct SpendableNote { } impl SpendableNote { - pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &ExtendedSpendingKey) -> Option { + pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &Option) -> Option { // Include only notes that haven't been spent, or haven't been included in an unconfirmed spend yet. - if nd.spent.is_none() && nd.unconfirmed_spent.is_none() && + if nd.spent.is_none() && nd.unconfirmed_spent.is_none() && extsk.is_some() && nd.witnesses.len() >= (anchor_offset + 1) { let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1); @@ -497,7 +496,7 @@ impl SpendableNote { diversifier: nd.diversifier, note: nd.note.clone(), witness: w.clone(), - extsk: extsk.clone(), + extsk: extsk.clone().unwrap(), }) } else { None diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index 40b5fa8..631d0c1 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -5,7 +5,7 @@ use rand::{RngCore, rngs::OsRng}; use ff::{Field, PrimeField}; use pairing::bls12_381::Bls12; use protobuf::{Message, UnknownFields, CachedSize, RepeatedField}; -use zcash_client_backend::{encoding::encode_payment_address, +use zcash_client_backend::{encoding::{encode_payment_address, decode_payment_address, decode_extended_spending_key, decode_extended_full_viewing_key}, proto::compact_formats::{ CompactBlock, CompactOutput, CompactSpend, CompactTx, } @@ -27,8 +27,9 @@ use zcash_primitives::{ use sha2::{Sha256, Digest}; -use super::LightWallet; +use super::{LightWallet}; use super::LightClientConfig; +use crate::lightwallet::walletzkey::{WalletZKeyType}; use secp256k1::{Secp256k1, key::PublicKey, key::SecretKey}; use crate::SaplingParams; @@ -293,12 +294,14 @@ fn test_z_balances() { let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); const AMOUNT1:u64 = 5; + let extfvk = wallet.zkeys.read().unwrap()[0].extfvk.clone(); + // Address is encoded in bech32 let address = Some(encode_payment_address(wallet.config.hrp_sapling_address(), - &wallet.extfvks.read().unwrap()[0].default_address().unwrap().1)); + &extfvk.default_address().unwrap().1)); let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); - cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); + cb1.add_tx_paying(extfvk.clone(), AMOUNT1); // Make sure that the intial state is empty assert_eq!(wallet.txs.read().unwrap().len(), 0); @@ -317,7 +320,7 @@ fn test_z_balances() { // Add a second block let mut cb2 = FakeCompactBlock::new(1, cb1.hash()); - cb2.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT2); + cb2.add_tx_paying(extfvk.clone(), AMOUNT2); wallet.scan_block(&cb2.as_bytes()).unwrap(); @@ -333,9 +336,10 @@ fn test_z_change_balances() { // First, add an incoming transaction const AMOUNT1:u64 = 5; + let extfvk = wallet.zkeys.read().unwrap()[0].extfvk.clone(); let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); - let (nf1, txid1) = cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); + let (nf1, txid1) = cb1.add_tx_paying(extfvk.clone(), AMOUNT1); wallet.scan_block(&cb1.as_bytes()).unwrap(); @@ -349,7 +353,7 @@ fn test_z_change_balances() { 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.read().unwrap()[0].clone(), addr2, AMOUNT2); + let txid2 = cb2.add_tx_spending((nf1, AMOUNT1), extfvk.clone(), addr2, AMOUNT2); wallet.scan_block(&cb2.as_bytes()).unwrap(); // Now, the original note should be spent and there should be a change @@ -517,9 +521,10 @@ fn test_serialization() { // First, add an incoming transaction const AMOUNT1:u64 = 5; + let extfvk = wallet.zkeys.read().unwrap()[0].extfvk.clone(); let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); - let (nf1, txid1) = cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); + let (nf1, txid1) = cb1.add_tx_paying(extfvk.clone(), AMOUNT1); wallet.scan_block(&cb1.as_bytes()).unwrap(); @@ -543,7 +548,7 @@ fn test_serialization() { 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.read().unwrap()[0].clone(), addr2, AMOUNT2); + let txid2 = cb2.add_tx_spending((nf1, AMOUNT1), extfvk.clone(), addr2, AMOUNT2); wallet.scan_block(&cb2.as_bytes()).unwrap(); let mut tx = FakeTransaction::new_with_txid(txid2); @@ -564,10 +569,8 @@ fn test_serialization() { { assert_eq!(wallet.seed, wallet2.seed); - 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.zaddress.read().unwrap()[0], wallet2.zaddress.read().unwrap()[0]); + assert_eq!(wallet.zkeys.read().unwrap().len(), wallet2.zkeys.read().unwrap().len()); + assert_eq!(wallet.zkeys.read().unwrap()[0], wallet2.zkeys.read().unwrap()[0]); assert_eq!(wallet.tkeys.read().unwrap().len(), wallet2.tkeys.read().unwrap().len()); assert_eq!(wallet.tkeys.read().unwrap()[0], wallet2.tkeys.read().unwrap()[0]); @@ -626,7 +629,7 @@ fn test_multi_serialization() { 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 (zaddr1, zpk1, zvk1) = &wallet.get_z_private_keys()[0]; let zaddr2 = wallet.add_zaddr(); let mut serialized_data = vec![]; @@ -634,17 +637,16 @@ fn test_multi_serialization() { 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.zaddress.read().unwrap().len(), 2); - + assert_eq!(wallet2.zkeys.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]; + let (w2_zaddr1, w2_zpk1, w2_zvk1) = &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!(zvk1, w2_zvk1); assert_eq!(zaddr2, *w2_zaddr2); } @@ -660,6 +662,23 @@ fn get_test_config() -> LightClientConfig { } } + +fn get_main_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, + } +} + +fn get_main_wallet() -> LightWallet { + let config = get_main_config(); + LightWallet::new(None, &config, 0).unwrap() +} + // Get a test wallet already setup with a single note fn get_test_wallet(amount: u64) -> (LightWallet, TxId, BlockHash) { let config = get_test_config(); @@ -667,7 +686,7 @@ fn get_test_wallet(amount: u64) -> (LightWallet, TxId, BlockHash) { 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.read().unwrap()[0].clone(), amount); + let (_, txid1) = cb1.add_tx_paying(wallet.zkeys.read().unwrap()[0].extfvk.clone(), amount); wallet.scan_block(&cb1.as_bytes()).unwrap(); // We have one note @@ -709,6 +728,7 @@ fn test_z_spend_to_z() { { assert_eq!(wallet.zbalance(None), AMOUNT1); assert_eq!(wallet.verified_zbalance(None), AMOUNT1); + assert_eq!(wallet.spendable_zbalance(None), AMOUNT1); } // Create a tx and send to address @@ -745,6 +765,7 @@ fn test_z_spend_to_z() { // The wallet should deduct this from the verified balance. The zbalance still includes it assert_eq!(wallet.zbalance(None), AMOUNT1); assert_eq!(wallet.verified_zbalance(None), 0); + assert_eq!(wallet.spendable_zbalance(None), 0); } let mut cb3 = FakeCompactBlock::new(2, block_hash); @@ -790,7 +811,6 @@ fn test_z_spend_to_z() { } } - #[test] fn test_self_txns_ttoz_withmemo() { let mut rng = OsRng; @@ -1007,14 +1027,14 @@ fn test_multi_z() { 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].extfvk, wallet.zkeys.read().unwrap()[0].extfvk); 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, 6); // The new addr is added after the change addresses + assert_eq!(txs[&sent_txid].notes[ext_note_number].extfvk, wallet.zkeys.read().unwrap()[6].extfvk); // The new addr is added after the change addresses 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); @@ -1261,7 +1281,7 @@ fn test_z_incoming_memo() { let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT1); let my_address = encode_payment_address(wallet.config.hrp_sapling_address(), - &wallet.extfvks.read().unwrap()[0].default_address().unwrap().1); + &wallet.zkeys.read().unwrap()[0].zaddress); let memo = "Incoming Memo".to_string(); let fee: u64 = DEFAULT_FEE.try_into().unwrap(); @@ -1288,7 +1308,7 @@ fn test_z_incoming_memo() { assert_eq!(txs[&sent_txid].notes.len(), 1); - assert_eq!(txs[&sent_txid].notes[0].extfvk, wallet.extfvks.read().unwrap()[0]); + assert_eq!(txs[&sent_txid].notes[0].extfvk, wallet.zkeys.read().unwrap()[0].extfvk); assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - fee); assert_eq!(LightWallet::note_address(wallet.config.hrp_sapling_address(), &txs[&sent_txid].notes[0]), Some(my_address)); assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[0].memo), Some(memo)); @@ -1302,7 +1322,7 @@ fn test_z_incoming_hex_memo() { let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT1); let my_address = encode_payment_address(wallet.config.hrp_sapling_address(), - &wallet.extfvks.read().unwrap()[0].default_address().unwrap().1); + &wallet.zkeys.read().unwrap()[0].zaddress); let orig_memo = "hello world".to_string(); let memo = format!("0x{}", hex::encode(&orig_memo)); @@ -1330,7 +1350,7 @@ fn test_z_incoming_hex_memo() { assert_eq!(txs[&sent_txid].notes.len(), 1); - assert_eq!(txs[&sent_txid].notes[0].extfvk, wallet.extfvks.read().unwrap()[0]); + assert_eq!(txs[&sent_txid].notes[0].extfvk, wallet.zkeys.read().unwrap()[0].extfvk); assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - fee); assert_eq!(LightWallet::note_address(wallet.config.hrp_sapling_address(), &txs[&sent_txid].notes[0]), Some(my_address)); assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[0].memo), Some(orig_memo)); @@ -1345,14 +1365,14 @@ fn test_add_new_zt_hd_after_incoming() { // Get the last address let my_address = encode_payment_address(wallet.config.hrp_sapling_address(), - &wallet.extfvks.read().unwrap().last().unwrap().default_address().unwrap().1); + &wallet.zkeys.read().unwrap().last().unwrap().zaddress); 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(); - assert_eq!(wallet.zaddress.read().unwrap().len(), 6); // Starts with 1+5 addresses + assert_eq!(wallet.zkeys.read().unwrap().len(), 6); // Starts with 1+5 addresses // Create a tx and send to the last address let raw_tx = wallet.send_to_address(branch_id, &ss, &so, @@ -1365,7 +1385,7 @@ fn test_add_new_zt_hd_after_incoming() { wallet.scan_block(&cb3.as_bytes()).unwrap(); // NOw, 5 new addresses should be created - assert_eq!(wallet.zaddress.read().unwrap().len(), 6+5); + assert_eq!(wallet.zkeys.read().unwrap().len(), 6+5); let mut rng = OsRng; let secp = Secp256k1::new(); @@ -1627,7 +1647,7 @@ fn test_multi_spends() { // Find zaddr2 let zaddr2_note = txs[&sent_txid].notes.iter().find(|n| n.note.value == ZAMOUNT2).unwrap(); - assert_eq!(zaddr2_note.account, 6); + assert_eq!(zaddr2_note.extfvk, wallet.zkeys.read().unwrap()[6].extfvk); assert_eq!(zaddr2_note.is_change, false); assert_eq!(zaddr2_note.spent, None); assert_eq!(zaddr2_note.unconfirmed_spent, None); @@ -1635,7 +1655,7 @@ fn test_multi_spends() { // Find zaddr3 let zaddr3_note = txs[&sent_txid].notes.iter().find(|n| n.note.value == ZAMOUNT3).unwrap(); - assert_eq!(zaddr3_note.account, 7); + assert_eq!(zaddr3_note.extfvk, wallet.zkeys.read().unwrap()[7].extfvk); assert_eq!(zaddr3_note.is_change, false); assert_eq!(zaddr3_note.spent, None); assert_eq!(zaddr3_note.unconfirmed_spent, None); @@ -1968,7 +1988,7 @@ fn test_rollback() { } #[test] -fn test_t_derivation() { +fn test_t_z_derivation() { let lc = LightClientConfig { server: "0.0.0.0:0".parse().unwrap(), chain_name: "main".to_string(), @@ -1993,9 +2013,10 @@ fn test_t_derivation() { assert_eq!(taddr, "t1NoS6ZgaUTpmjkge2cVpXGcySasdYDrXqh"); assert_eq!(pk, "KxdmS38pxskS6bbKX43zhTu8ppWckNmWjKsQFX1hwidvhRRgRd3c"); - let (zaddr, sk) = &wallet.get_z_private_keys()[0]; + let (zaddr, sk, vk) = &wallet.get_z_private_keys()[0]; assert_eq!(zaddr, "zs1q6xk3q783t5k92kjqt2rkuuww8pdw2euzy5rk6jytw97enx8fhpazdv3th4xe7vsk6e9sfpawfg"); assert_eq!(sk, "secret-extended-key-main1qvpa0qr8qqqqpqxn4l054nzxpxzp3a8r2djc7sekdek5upce8mc2j2z0arzps4zv940qeg706hd0wq6g5snzvhp332y6vhwyukdn8dhekmmsk7fzvzkqm6ypc99uy63tpesqwxhpre78v06cx8k5xpp9mrhtgqs5dvp68cqx2yrvthflmm2ynl8c0506dekul0f6jkcdmh0292lpphrksyc5z3pxwws97zd5els3l2mjt2s7hntap27mlmt6w0drtfmz36vz8pgu7ec0twfrq"); + assert_eq!(vk, "zxviews1qvpa0qr8qqqqpqxn4l054nzxpxzp3a8r2djc7sekdek5upce8mc2j2z0arzps4zv9kdvg28gjzvxd47ant6jn4svln5psw3htx93cq93ahw4e7lptrtlq7he5r6p6rcm3s0z6l24ype84sgqfrmghu449htrjspfv6qg2zfx2yrvthflmm2ynl8c0506dekul0f6jkcdmh0292lpphrksyc5z3pxwws97zd5els3l2mjt2s7hntap27mlmt6w0drtfmz36vz8pgu7ecrxzsls"); assert_eq!(seed_phrase, Some(wallet.get_seed_phrase())); } @@ -2009,7 +2030,7 @@ fn test_lock_unlock() { // Add some addresses let zaddr0 = encode_payment_address(config.hrp_sapling_address(), - &wallet.extfvks.read().unwrap()[0].default_address().unwrap().1); + &wallet.zkeys.read().unwrap()[0].zaddress); let zaddr1 = wallet.add_zaddr(); // This is actually address at index 6 let zaddr2 = wallet.add_zaddr(); // This is actually address at index 7 @@ -2028,6 +2049,10 @@ fn test_lock_unlock() { // Encrypting an already encrypted wallet should fail assert!(wallet.encrypt("somepassword".to_string()).is_err()); + // Adding a new key while the wallet is locked is an error + assert!(wallet.add_taddr().starts_with("Error")); + assert!(wallet.add_zaddr().starts_with("Error")); + // Serialize a locked wallet let mut serialized_data = vec![]; wallet.write(&mut serialized_data).expect("Serialize wallet"); @@ -2040,7 +2065,7 @@ fn test_lock_unlock() { assert_eq!(seed, wallet.seed); { - let extsks = wallet.extsks.read().unwrap(); + let extsks: Vec = wallet.zkeys.read().unwrap().iter().map(|zk| zk.extsk.clone().unwrap()).collect(); let tkeys = wallet.tkeys.read().unwrap(); assert_eq!(extsks.len(), 8); // 3 zaddrs + 1 original + 4 extra HD addreses assert_eq!(tkeys.len(), 3); @@ -2076,7 +2101,7 @@ fn test_lock_unlock() { assert_eq!(seed, wallet2.seed); { - let extsks = wallet2.extsks.read().unwrap(); + let extsks: Vec = wallet2.zkeys.read().unwrap().iter().map(|zk| zk.extsk.clone().unwrap()).collect(); let tkeys = wallet2.tkeys.read().unwrap(); assert_eq!(extsks.len(), 8); assert_eq!(tkeys.len(), 3); @@ -2118,6 +2143,174 @@ fn test_lock_unlock() { assert!(wallet2.unlock("newpassword".to_string()).is_err()); } +#[test] +fn test_import_birthday_adjust() { + let config = LightClientConfig { + server: "0.0.0.0:0".parse().unwrap(), + chain_name: "main".to_string(), + sapling_activation_height: 5, + consensus_branch_id: "000000".to_string(), + anchor_offset: 0, + data_dir: None, + }; + + let privkey = "secret-extended-key-main1q0p44m9zqqqqpqyxfvy5w2vq6ahvxyrwsk2w4h2zleun4cft4llmnsjlv77lhuuknv6x9jgu5g2clf3xq0wz9axxxq8klvv462r5pa32gjuj5uhxnvps6wsrdg6xll05unwks8qpgp4psmvy5e428uxaggn4l29duk82k3sv3njktaaj453fdmfmj2fup8rls4egqxqtj2p5a3yt4070khn99vzxj5ag5qjngc4v2kq0ctl9q2rpc2phu4p3e26egu9w88mchjf83sqgh3cev"; + + { + let mut wallet = LightWallet::new(None, &config, 10).unwrap(); + assert_eq!(wallet.birthday, 10); + + // Import key with birthday after the current birthday + wallet.add_imported_sk(privkey.to_string(), 15); + assert_eq!(wallet.birthday, 10); + } + + { + let mut wallet = LightWallet::new(None, &config, 10).unwrap(); + assert_eq!(wallet.birthday, 10); + + // Import key with birthday before the current birthday + wallet.add_imported_sk(privkey.to_string(), 7); + assert_eq!(wallet.birthday, 7); + } + + { + let mut wallet = LightWallet::new(None, &config, 10).unwrap(); + assert_eq!(wallet.birthday, 10); + + // Import key with birthday before the sapling activation + wallet.add_imported_sk(privkey.to_string(), 3); + assert_eq!(wallet.birthday, 5); + } +} + +#[test] +fn test_import_sk() { + let mut wallet = get_main_wallet(); + + // Priv Key's address + let zaddr = "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv".to_string(); + let privkey = "secret-extended-key-main1q0p44m9zqqqqpqyxfvy5w2vq6ahvxyrwsk2w4h2zleun4cft4llmnsjlv77lhuuknv6x9jgu5g2clf3xq0wz9axxxq8klvv462r5pa32gjuj5uhxnvps6wsrdg6xll05unwks8qpgp4psmvy5e428uxaggn4l29duk82k3sv3njktaaj453fdmfmj2fup8rls4egqxqtj2p5a3yt4070khn99vzxj5ag5qjngc4v2kq0ctl9q2rpc2phu4p3e26egu9w88mchjf83sqgh3cev"; + + assert_eq!(wallet.add_imported_sk(privkey.to_string(), 0), zaddr); + assert_eq!(wallet.get_all_zaddresses().len(), 2); + assert_eq!(wallet.get_all_zaddresses()[1], zaddr); + assert_eq!(wallet.zkeys.read().unwrap()[1].keytype, WalletZKeyType::ImportedSpendingKey); + assert_eq!(wallet.zkeys.read().unwrap()[1].hdkey_num, None); + + // Importing it again should fail + assert!(wallet.add_imported_sk(privkey.to_string(), 0).starts_with("Error")); + assert_eq!(wallet.get_all_zaddresses().len(), 2); + + // Now, adding a new z address should create a new HD key + let new_zaddr = wallet.add_zaddr(); + assert_eq!(wallet.get_all_zaddresses().len(), 3); + assert_eq!(wallet.get_all_zaddresses()[2], new_zaddr); + assert_eq!(wallet.zkeys.read().unwrap()[2].keytype, WalletZKeyType::HdKey); + assert_eq!(wallet.zkeys.read().unwrap()[2].hdkey_num, Some(1)); + + // Encrypt it + let passwd = "password".to_string(); + assert!(wallet.encrypt(passwd.clone()).is_ok()); + assert_eq!(wallet.zkeys.read().unwrap()[1].extsk, None); + assert_eq!(wallet.zkeys.read().unwrap()[1].zaddress, decode_payment_address(wallet.config.hrp_sapling_address(), &zaddr).unwrap().unwrap()); + + // Unlock it + assert!(wallet.unlock(passwd.clone()).is_ok()); + assert_eq!(wallet.zkeys.read().unwrap()[1].extsk, decode_extended_spending_key(wallet.config.hrp_sapling_private_key(), &privkey).unwrap()); + assert_eq!(wallet.zkeys.read().unwrap()[1].zaddress, decode_payment_address(wallet.config.hrp_sapling_address(), &zaddr).unwrap().unwrap()); + + // Remove encryption + assert!(wallet.remove_encryption(passwd).is_ok()); + assert_eq!(wallet.zkeys.read().unwrap()[1].extsk, decode_extended_spending_key(wallet.config.hrp_sapling_private_key(), &privkey).unwrap()); + assert_eq!(wallet.zkeys.read().unwrap()[1].zaddress, decode_payment_address(wallet.config.hrp_sapling_address(), &zaddr).unwrap().unwrap()); +} + + +#[test] +fn test_import_vk() { + let mut wallet = get_main_wallet(); + + // Priv Key's address + let zaddr= "zs1va5902apnzlhdu0pw9r9q7ca8s4vnsrp2alr6xndt69jnepn2v2qrj9vg3wfcnjyks5pg65g9dc"; + let viewkey = "zxviews1qvvx7cqdqyqqpqqte7292el2875kw2fgvnkmlmrufyszlcy8xgstwarnumqye3tr3d9rr3ydjm9zl9464majh4pa3ejkfy779dm38sfnkar67et7ykxkk0z9rfsmf9jclfj2k85xt2exkg4pu5xqyzyxzlqa6x3p9wrd7pwdq2uvyg0sal6zenqgfepsdp8shestvkzxuhm846r2h3m4jvsrpmxl8pfczxq87886k0wdasppffjnd2eh47nlmkdvrk6rgyyl0ekh3ycqtvvje"; + + assert_eq!(wallet.add_imported_vk(viewkey.to_string(), 0), zaddr); + assert_eq!(wallet.get_all_zaddresses().len(), 2); + assert_eq!(wallet.get_all_zaddresses()[1], zaddr); + assert_eq!(wallet.zkeys.read().unwrap()[1].keytype, WalletZKeyType::ImportedViewKey); + assert_eq!(wallet.zkeys.read().unwrap()[1].hdkey_num, None); + + // Importing it again should fail + assert!(wallet.add_imported_sk(viewkey.to_string(), 0).starts_with("Error")); + assert_eq!(wallet.get_all_zaddresses().len(), 2); + + // Now, adding a new z address should create a new HD key + let new_zaddr = wallet.add_zaddr(); + assert_eq!(wallet.get_all_zaddresses().len(), 3); + assert_eq!(wallet.get_all_zaddresses()[2], new_zaddr); + assert_eq!(wallet.zkeys.read().unwrap()[2].keytype, WalletZKeyType::HdKey); + assert_eq!(wallet.zkeys.read().unwrap()[2].hdkey_num, Some(1)); + + // Encrypt it + let passwd = "password".to_string(); + assert!(wallet.encrypt(passwd.clone()).is_ok()); + assert_eq!(wallet.zkeys.read().unwrap()[1].extsk, None); + assert_eq!(wallet.zkeys.read().unwrap()[1].extfvk, decode_extended_full_viewing_key(wallet.config.hrp_sapling_viewing_key(), viewkey).unwrap().unwrap()); + assert_eq!(wallet.zkeys.read().unwrap()[1].zaddress, decode_payment_address(wallet.config.hrp_sapling_address(), &zaddr).unwrap().unwrap()); + + // Unlock it + assert!(wallet.unlock(passwd.clone()).is_ok()); + assert_eq!(wallet.zkeys.read().unwrap()[1].extsk, None); + assert_eq!(wallet.zkeys.read().unwrap()[1].extfvk, decode_extended_full_viewing_key(wallet.config.hrp_sapling_viewing_key(), viewkey).unwrap().unwrap()); + assert_eq!(wallet.zkeys.read().unwrap()[1].zaddress, decode_payment_address(wallet.config.hrp_sapling_address(), &zaddr).unwrap().unwrap()); + + // Remove encryption + assert!(wallet.remove_encryption(passwd).is_ok()); + assert_eq!(wallet.zkeys.read().unwrap()[1].extsk, None); + assert_eq!(wallet.zkeys.read().unwrap()[1].extfvk, decode_extended_full_viewing_key(wallet.config.hrp_sapling_viewing_key(), viewkey).unwrap().unwrap()); + assert_eq!(wallet.zkeys.read().unwrap()[1].zaddress, decode_payment_address(wallet.config.hrp_sapling_address(), &zaddr).unwrap().unwrap()); +} + +#[test] +fn test_import_sk_upgrade_vk() { + // Test where we import the viewkey first, then upgrade it to the full spending key + + let zaddr= "zs1va5902apnzlhdu0pw9r9q7ca8s4vnsrp2alr6xndt69jnepn2v2qrj9vg3wfcnjyks5pg65g9dc"; + let viewkey = "zxviews1qvvx7cqdqyqqpqqte7292el2875kw2fgvnkmlmrufyszlcy8xgstwarnumqye3tr3d9rr3ydjm9zl9464majh4pa3ejkfy779dm38sfnkar67et7ykxkk0z9rfsmf9jclfj2k85xt2exkg4pu5xqyzyxzlqa6x3p9wrd7pwdq2uvyg0sal6zenqgfepsdp8shestvkzxuhm846r2h3m4jvsrpmxl8pfczxq87886k0wdasppffjnd2eh47nlmkdvrk6rgyyl0ekh3ycqtvvje"; + let privkey = "secret-extended-key-main1qvvx7cqdqyqqpqqte7292el2875kw2fgvnkmlmrufyszlcy8xgstwarnumqye3tr3w3p4qn52e9htagqfejxlkq4m35wmfqvua7dxf8saqqhhfwvf0lqv27cz45uk5na9wwr27u607gu0x92phg6twpsm84pyyjnvtdqkggvq2uvyg0sal6zenqgfepsdp8shestvkzxuhm846r2h3m4jvsrpmxl8pfczxq87886k0wdasppffjnd2eh47nlmkdvrk6rgyyl0ekh3yccw7kmg"; + + let mut wallet = get_main_wallet(); + + assert_eq!(wallet.add_imported_vk(viewkey.to_string(), 0), zaddr); + assert_eq!(wallet.get_all_zaddresses().len(), 2); + assert_eq!(wallet.get_all_zaddresses()[1], zaddr); + assert_eq!(wallet.zkeys.read().unwrap()[1].keytype, WalletZKeyType::ImportedViewKey); + assert_eq!(wallet.zkeys.read().unwrap()[1].hdkey_num, None); + assert!(wallet.zkeys.read().unwrap()[1].extsk.is_none()); + + // Importing it again should fail because it already exists + assert!(wallet.add_imported_sk(viewkey.to_string(), 0).starts_with("Error")); + assert_eq!(wallet.get_all_zaddresses().len(), 2); + + // Now, adding a new z address should create a new HD key + let new_zaddr = wallet.add_zaddr(); + assert_eq!(wallet.get_all_zaddresses().len(), 3); + assert_eq!(wallet.get_all_zaddresses()[2], new_zaddr); + assert_eq!(wallet.zkeys.read().unwrap()[2].keytype, WalletZKeyType::HdKey); + assert_eq!(wallet.zkeys.read().unwrap()[2].hdkey_num, Some(1)); + + // Now import the privkey for the existing viewing key + assert_eq!(wallet.add_imported_sk(privkey.to_string(), 0), zaddr); + assert_eq!(wallet.get_all_zaddresses().len(), 3); + assert_eq!(wallet.get_all_zaddresses()[1], zaddr); + + // Should now be a spending key + assert_eq!(wallet.zkeys.read().unwrap()[1].keytype, WalletZKeyType::ImportedSpendingKey); + assert_eq!(wallet.zkeys.read().unwrap()[1].hdkey_num, None); + assert!(wallet.zkeys.read().unwrap()[1].extsk.is_some()); +} + #[test] #[should_panic] fn test_invalid_bip39_t() { @@ -2269,7 +2462,7 @@ fn test_encrypted_zreceive() { // Find zaddr2 let zaddr2_note = txs[&txid2].notes.iter().find(|n| n.note.value == ZAMOUNT2).unwrap(); - assert_eq!(zaddr2_note.account, 6); + assert_eq!(zaddr2_note.extfvk, wallet.zkeys.read().unwrap()[6].extfvk); assert_eq!(zaddr2_note.is_change, false); assert_eq!(zaddr2_note.spent, None); assert_eq!(zaddr2_note.unconfirmed_spent, None); diff --git a/lib/src/lightwallet/walletzkey.rs b/lib/src/lightwallet/walletzkey.rs new file mode 100644 index 0000000..d6c3a1f --- /dev/null +++ b/lib/src/lightwallet/walletzkey.rs @@ -0,0 +1,421 @@ +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; + +#[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, + pub(super) extfvk: ExtendedFullViewingKey, + pub(super) zaddress: PaymentAddress, + + // If this is a HD key, what is the key number + pub(super) hdkey_num: Option, + + // If locked, the encrypted private key is stored here + enc_key: Option>, + nonce: Option>, +} + +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 read(mut inp: R) -> io::Result { + let version = inp.read_u8()?; + assert!(version <= Self::serialized_version()); + + let keytype: WalletZKeyType = match inp.read_u32::()? { + 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::())?; + + 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(&self, mut out: W) -> io::Result<()> { + out.write_u8(Self::serialized_version())?; + + out.write_u32::(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::(*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 = 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); + } + } + + +} \ No newline at end of file From 49ee4c40679d2518aed4f72477956b65269c07eb Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Tue, 21 Jul 2020 16:13:56 -0700 Subject: [PATCH 05/21] Add spent_at_height for notes --- lib/src/lightclient.rs | 2 ++ lib/src/lightwallet.rs | 57 +++++++++++++++++++++++++++++++++---- lib/src/lightwallet/data.rs | 17 +++++++++-- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 570259e..efa6206 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -835,6 +835,8 @@ impl LightClient { "is_change" => nd.is_change, "address" => LightWallet::note_address(self.config.hrp_sapling_address(), nd), "spent" => nd.spent.map(|spent_txid| format!("{}", spent_txid)), + "spent_at_height" => nd.spent_at_height.map(|h| format!("{}", h)), + "witness_size" => nd.witnesses.len(), "unconfirmed_spent" => nd.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), }) } diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index a3b0459..e6abc3d 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -143,7 +143,7 @@ pub struct LightWallet { impl LightWallet { pub fn serialized_version() -> u64 { - return 7; + return 8; } fn get_taddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> SecretKey { @@ -379,7 +379,7 @@ impl LightWallet { let birthday = reader.read_u64::()?; - Ok(LightWallet{ + let lw = LightWallet{ encrypted: encrypted, unlocked: !encrypted, // When reading from disk, if wallet is encrypted, it starts off locked. enc_seed: enc_seed, @@ -394,7 +394,14 @@ impl LightWallet { config: config.clone(), birthday, total_scan_duration: Arc::new(RwLock::new(vec![Duration::new(0, 0)])), - }) + }; + + // Do a one-time fix of the spent_at_height for older wallets + if version <= 7 { + lw.fix_spent_at_height(); + } + + Ok(lw) } pub fn write(&self, mut writer: W) -> io::Result<()> { @@ -1686,6 +1693,16 @@ impl LightWallet { // Create a write lock let mut txs = self.txs.write().unwrap(); + // Trim the older witnesses + txs.values_mut().for_each(|wtx| { + wtx.notes + .iter_mut() + .filter(|nd| nd.spent.is_some() && nd.spent_at_height.is_some() && nd.spent_at_height.unwrap() < height - (MAX_REORG as i32) - 1) + .for_each(|nd| { + nd.witnesses.clear() + }) + }); + // Create a Vec containing all unspent nullifiers. // Include only the confirmed spent nullifiers, since unconfirmed ones still need to be included // during scan_block below. @@ -1723,11 +1740,22 @@ impl LightWallet { let nf_refs = nfs.iter().map(|(nf, account, _)| (nf.to_vec(), *account)).collect::>(); let extfvks: Vec = self.zkeys.read().unwrap().iter().map(|zk| zk.extfvk.clone()).collect(); - // Create a single mutable slice of all the newly-added witnesses. + // Create a single mutable slice of all the wallet's note's witnesses. let mut witness_refs: Vec<_> = txs .values_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 })) + .map(|tx| + tx.notes.iter_mut() + .filter_map(|nd| + // Note was not spent + if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { + nd.witnesses.last_mut() + } else if nd.spent.is_some() && nd.spent_at_height.is_some() && nd.spent_at_height.unwrap() < height - (MAX_REORG as i32) - 1 { + // Note was spent in the last 100 blocks + nd.witnesses.last_mut() + } else { + // If note was old (spent NOT in the last 100 blocks) + None + })) .flatten() .collect(); @@ -1780,6 +1808,7 @@ impl LightWallet { // Mark the note as spent, and remove the unconfirmed part of it info!("Marked a note as spent"); spent_note.spent = Some(tx.txid); + spent_note.spent_at_height = Some(height); spent_note.unconfirmed_spent = None::; total_shielded_value_spent += spent_note.note.value; @@ -1842,6 +1871,22 @@ impl LightWallet { Ok(all_txs) } + // Add the spent_at_height for each sapling note that has been spent. This field was added in wallet version 8, + // so for older wallets, it will need to be added + pub fn fix_spent_at_height(&self) { + // First, build an index of all the txids and the heights at which they were spent. + let spent_txid_map: HashMap<_, _> = self.txs.read().unwrap().iter().map(|(txid, wtx)| (txid.clone(), wtx.block)).collect(); + + // Go over all the sapling notes that might need updating + self.txs.write().unwrap().values_mut().for_each(|wtx| { + wtx.notes.iter_mut() + .filter(|nd| nd.spent.is_some() && nd.spent_at_height.is_none()) + .for_each(|nd| { + nd.spent_at_height = spent_txid_map.get(&nd.spent.unwrap()).map(|b| *b); + }) + }); + } + pub fn send_to_address( &self, consensus_branch_id: u32, diff --git a/lib/src/lightwallet/data.rs b/lib/src/lightwallet/data.rs index 5354e52..11864de 100644 --- a/lib/src/lightwallet/data.rs +++ b/lib/src/lightwallet/data.rs @@ -65,9 +65,10 @@ pub struct SaplingNoteData { pub(super) extfvk: ExtendedFullViewingKey, // Technically, this should be recoverable from the account number, but we're going to refactor this in the future, so I'll write it again here. pub diversifier: Diversifier, pub note: Note, - pub(super) witnesses: Vec>, + pub witnesses: Vec>, pub(super) nullifier: [u8; 32], pub spent: Option, // If this note was confirmed spent + pub spent_at_height: Option, // The height at which this note was spent pub unconfirmed_spent: Option, // If this note was spent in a send, but has not yet been confirmed. pub memo: Option, pub is_change: bool, @@ -106,7 +107,7 @@ pub fn read_note(mut reader: R) -> io::Result<(u64, Fs)> { impl SaplingNoteData { fn serialized_version() -> u64 { - 1 + 2 } pub fn new( @@ -132,6 +133,7 @@ impl SaplingNoteData { witnesses: vec![witness], nullifier: nf, spent: None, + spent_at_height: None, unconfirmed_spent: None, memo: None, is_change: output.is_change, @@ -140,7 +142,7 @@ impl SaplingNoteData { // Reading a note also needs the corresponding address to read from. pub fn read(mut reader: R) -> io::Result { - let _version = reader.read_u64::()?; + let version = reader.read_u64::()?; let account = reader.read_u64::()? as usize; @@ -175,6 +177,12 @@ impl SaplingNoteData { Ok(TxId{0: txid_bytes}) })?; + let spent_at_height = if version >=2 { + Optional::read(&mut reader, |r| r.read_i32::())? + } else { + None + }; + let memo = Optional::read(&mut reader, |r| { let mut memo_bytes = [0u8; 512]; r.read_exact(&mut memo_bytes)?; @@ -194,6 +202,7 @@ impl SaplingNoteData { witnesses, nullifier, spent, + spent_at_height, unconfirmed_spent: None, memo, is_change, @@ -221,6 +230,8 @@ impl SaplingNoteData { writer.write_all(&self.nullifier)?; Optional::write(&mut writer, &self.spent, |w, t| w.write_all(&t.0))?; + Optional::write(&mut writer, &self.spent_at_height, |w, h| w.write_i32::(*h))?; + Optional::write(&mut writer, &self.memo, |w, m| w.write_all(m.as_bytes()))?; writer.write_u8(if self.is_change {1} else {0})?; From a915709dcbfc7d6afd59ac36e6bc3460881e34d5 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Tue, 21 Jul 2020 16:52:47 -0700 Subject: [PATCH 06/21] Fix import when encrpted --- lib/src/lightwallet.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index e6abc3d..15e3f11 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -568,8 +568,8 @@ impl LightWallet { // 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(); + if self.encrypted { + return "Error: Can't import spending key while wallet is encrypted".to_string(); } // First, try to interpret the key From 49a91a9a29cd7b6ebc972893a0b70f943f2c73d6 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Wed, 22 Jul 2020 09:53:28 -0700 Subject: [PATCH 07/21] 910k checkpoint --- lib/src/lightclient/checkpoints.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/src/lightclient/checkpoints.rs b/lib/src/lightclient/checkpoints.rs index ffc94b0..166304b 100644 --- a/lib/src/lightclient/checkpoints.rs +++ b/lib/src/lightclient/checkpoints.rs @@ -49,6 +49,9 @@ fn get_main_checkpoint(height: u64) -> Option<(u64, &'static str, &'static str) (870000, "0000000001097864030cac84b7bb93d12739ce9391612c66758c76e3243f0306", "01302886fbfdb837d575fc8fc2d8a7f74cb62a19ca60d2651eb19c5e6f486a4e22014408f734c0d7c683f37021404694b91dba5a0831c19035041c6bea83889af76912013cfc980f1a52aa4f2eb962c0b7bbd89e1a7e1f00dd1c8a62c0e03f118b4eb65b01cfe618a71029cf0bc39e796eeedc70ff9402959487f5825b5b15ede34b36021401bd79f262082e6a1ebdf586cd9c5b4726afd2d85bfb951eef97fb90537de86723012bc7fba59d8ac7d6c7d8538446b2dfcde28ee3c26f445938c8966196da9d456e019a599970b9798b4dc0f0ee59dd273a70b209515d95c510117db7eecebb36e80301c70fe44f3142eb00cc4f28d13d42173ce9e7f316e987c8fc3a2e01ee3b71bd2400000108348cb2dfc1ff7af214ad16c6bdb1d49ace2176a6aacea4d6ddc9d3a7cb9334000000000001166bb2e71749ab956e549119ce9099df3dbb053409ff89d0d86a17d5b02d015d0000011323ddf890bfd7b94fc609b0d191982cb426b8bf4d900d04709a8b9cb1a27625" ), + (910000, "0000000000f90b683c2a3fef74e21872247b19ad4558b36ca3eea9405538cd70", + "010ece000f48ffec735cf7756501aa52be1a7265a37b388b25966659eb8ae45a400110f701d1ea41c927f85f6564a512b295b36eeee186394a88bf00863a7900fa591201fe016c729c11a542f7b044299c8125892390d8f62fa853b53e1eb07e20fc64450001e1ba264bfa5f4a14498e5ac811acae9ebc9bdca547f6dd851dd3a2eefaab0c2c0001bec5ba9b735a95467fa0e1fafd2090b29765f323deb07e79757722e5cd77b835017217b253186b5fb1b0d55959b7e77e520c29363b6ba8a9f73932fa42fa09c3530001e63960c1f08d2cc7bc0f141dbee83516a13fd52252192047936aff6ba1cf4d620130978181e2608adad8aefcf44a3bf56387265b35ccbd619c8930631c4364c03f00000133604c7020edaefee31d4a419a4067ccd09d369a43fe4c032eeb7081774ed53901390f92d515d7c479d7c9807949f237c50bc77a732f0853884f12d01b72a5e75401d3ddf137881180d7c5fd73d188c4346291168bde688643855eb3cd5f680f9c0001166bb2e71749ab956e549119ce9099df3dbb053409ff89d0d86a17d5b02d015d0000011323ddf890bfd7b94fc609b0d191982cb426b8bf4d900d04709a8b9cb1a27625" + ), ]; find_checkpoint(height, checkpoints) From 25108df815d9d3fcaf51121fe1e6c6ff314eff2a Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Wed, 22 Jul 2020 10:18:13 -0700 Subject: [PATCH 08/21] Remove witness site from JSON --- lib/src/lightclient.rs | 1 - lib/src/lightwallet/data.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index efa6206..622c019 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -836,7 +836,6 @@ impl LightClient { "address" => LightWallet::note_address(self.config.hrp_sapling_address(), nd), "spent" => nd.spent.map(|spent_txid| format!("{}", spent_txid)), "spent_at_height" => nd.spent_at_height.map(|h| format!("{}", h)), - "witness_size" => nd.witnesses.len(), "unconfirmed_spent" => nd.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), }) } diff --git a/lib/src/lightwallet/data.rs b/lib/src/lightwallet/data.rs index 11864de..c0d6bf5 100644 --- a/lib/src/lightwallet/data.rs +++ b/lib/src/lightwallet/data.rs @@ -65,7 +65,7 @@ pub struct SaplingNoteData { pub(super) extfvk: ExtendedFullViewingKey, // Technically, this should be recoverable from the account number, but we're going to refactor this in the future, so I'll write it again here. pub diversifier: Diversifier, pub note: Note, - pub witnesses: Vec>, + pub(super) witnesses: Vec>, pub(super) nullifier: [u8; 32], pub spent: Option, // If this note was confirmed spent pub spent_at_height: Option, // The height at which this note was spent From 4a16b282ecb7fe09f4dfe2a1eaa8a3a0b8a6392a Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Wed, 22 Jul 2020 10:18:34 -0700 Subject: [PATCH 09/21] Add witness size + import while encrypted tests --- lib/src/lightwallet/tests.rs | 112 +++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index 631d0c1..c74bf25 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -707,6 +707,81 @@ fn get_test_wallet(amount: u64) -> (LightWallet, TxId, BlockHash) { (wallet, txid1, cb2.hash()) } +#[test] +fn test_witness_updates() { + const AMOUNT1: u64 = 50000; + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); + let mut phash = block_hash; + + // 2 blocks, so 2 witnesses to start with + assert_eq!(wallet.txs.read().unwrap().get(&txid1).unwrap().notes[0].witnesses.len(), 2); + + // Add 2 new blocks + for i in 2..4 { + let blk = FakeCompactBlock::new(i, phash); + wallet.scan_block(&blk.as_bytes()).unwrap(); + phash = blk.hash(); + } + + // 2 blocks, so now 4 total witnesses + assert_eq!(wallet.txs.read().unwrap().get(&txid1).unwrap().notes[0].witnesses.len(), 4); + + // Now add a 100 new blocks, the witness size should max out at 100 + for i in 4..104 { + let blk = FakeCompactBlock::new(i, phash); + wallet.scan_block(&blk.as_bytes()).unwrap(); + phash = blk.hash(); + } + assert_eq!(wallet.txs.read().unwrap().get(&txid1).unwrap().notes[0].witnesses.len(), 100); + + // Now spend the funds + 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: u64 = 20; + 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, + vec![(&ext_address, AMOUNT_SENT, None)]).unwrap(); + + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid = sent_tx.txid(); + + let mut cb3 = FakeCompactBlock::new(104, phash); + cb3.add_tx(&sent_tx); + wallet.scan_block(&cb3.as_bytes()).unwrap(); + phash = cb3.hash(); + + // 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].spent_at_height, Some(104)); + } + + assert_eq!(wallet.txs.read().unwrap().get(&txid1).unwrap().notes[0].witnesses.len(), 100); + + // Add new blocks, but the witness should still get updated + for i in 105..110 { + let blk = FakeCompactBlock::new(i, phash); + wallet.scan_block(&blk.as_bytes()).unwrap(); + phash = blk.hash(); + } + assert_eq!(wallet.txs.read().unwrap().get(&txid1).unwrap().notes[0].witnesses.len(), 100); + + // And after a 100 new blocks, now the witness should be empty + for i in 110..210 { + let blk = FakeCompactBlock::new(i, phash); + wallet.scan_block(&blk.as_bytes()).unwrap(); + phash = blk.hash(); + } + assert_eq!(wallet.txs.read().unwrap().get(&txid1).unwrap().notes[0].witnesses.len(), 0); +} + #[test] fn test_z_spend_to_z() { const AMOUNT1: u64 = 50000; @@ -2227,6 +2302,43 @@ fn test_import_sk() { } +#[test] +fn test_import_sk_while_encrypted() { + let mut wallet = get_main_wallet(); + + // Priv Key's address + let zaddr = "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv".to_string(); + let privkey = "secret-extended-key-main1q0p44m9zqqqqpqyxfvy5w2vq6ahvxyrwsk2w4h2zleun4cft4llmnsjlv77lhuuknv6x9jgu5g2clf3xq0wz9axxxq8klvv462r5pa32gjuj5uhxnvps6wsrdg6xll05unwks8qpgp4psmvy5e428uxaggn4l29duk82k3sv3njktaaj453fdmfmj2fup8rls4egqxqtj2p5a3yt4070khn99vzxj5ag5qjngc4v2kq0ctl9q2rpc2phu4p3e26egu9w88mchjf83sqgh3cev"; + + + // Encrypt it + let passwd = "password".to_string(); + assert!(wallet.encrypt(passwd.clone()).is_ok()); + + // Importing it should fail, because we can't import into an encrypted wallet + assert!(wallet.add_imported_sk(privkey.to_string(), 0).starts_with("Error")); + assert_eq!(wallet.get_all_zaddresses().len(), 1); + + // Unlock it + assert!(wallet.unlock(passwd.clone()).is_ok()); + + // Importing it should still fail, as even an unlocked wallet can't import a private key + assert!(wallet.add_imported_sk(privkey.to_string(), 0).starts_with("Error")); + assert_eq!(wallet.get_all_zaddresses().len(), 1); + + // Remove encryption + assert!(wallet.remove_encryption(passwd).is_ok()); + + // Now, import should work. + assert_eq!(wallet.add_imported_sk(privkey.to_string(), 0), zaddr); + assert_eq!(wallet.get_all_zaddresses().len(), 2); + assert_eq!(wallet.get_all_zaddresses()[1], zaddr); + assert_eq!(wallet.zkeys.read().unwrap()[1].keytype, WalletZKeyType::ImportedSpendingKey); + assert_eq!(wallet.zkeys.read().unwrap()[1].hdkey_num, None); + +} + + #[test] fn test_import_vk() { let mut wallet = get_main_wallet(); From c117c5d739354f67a6cd8fbb996e0cab2e389792 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Thu, 23 Jul 2020 10:52:38 -0700 Subject: [PATCH 10/21] Accept JSON for import --- lib/src/commands.rs | 55 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/lib/src/commands.rs b/lib/src/commands.rs index fadd7f2..349de1e 100644 --- a/lib/src/commands.rs +++ b/lib/src/commands.rs @@ -660,6 +660,8 @@ impl Command for ImportCommand { h.push("Import an external spending or viewing key into the wallet"); h.push("Usage:"); h.push("import [norescan]"); + h.push("OR"); + h.push("import '{'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."); @@ -673,27 +675,54 @@ impl Command for ImportCommand { } fn exec(&self, args: &[&str], lightclient: &LightClient) -> String { - if args.len() < 2 || args.len() > 3 { + if args.len() == 0 || args.len() > 3 { return format!("Insufficient arguments\n\n{}", self.help()); } - let key = args[0]; - let birthday = match args[1].parse::() { - Ok(b) => b, - Err(_) => return format!("Couldn't parse {} as birthday. Please specify an integer. Ok to use '0'", args[1]), - }; + let (key, birthday, rescan) = if args.len() == 1 { + // If only one arg, parse it as JSON + let json_args = match json::parse(&args[0]) { + Ok(j) => j, + Err(e) => { + let es = format!("Couldn't understand JSON: {}", e); + return format!("{}\n{}", es, self.help()); + } + }; - 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]); + if !json_args.is_object() { + return format!("Couldn't parse argument as a JSON object\n{}", self.help()); } + + if !json_args.has_key("key") { + return format!("'key' field is required in the JSON, containing the spending or viewing key to import\n{}", self.help()); + } + + if !json_args.has_key("birthday") { + return format!("'birthday' field is required in the JSON, containing the birthday of the spending or viewing key\n{}", self.help()); + } + + (json_args["key"].as_str().unwrap().to_string(), json_args["birthday"].as_u64().unwrap(), !json_args.has_key("norescan")) } else { - true + let key = args[0]; + let birthday = match args[1].parse::() { + Ok(b) => b, + Err(_) => return format!("Couldn't parse {} as birthday. Please specify an integer. Ok to use '0'", args[1]), + }; + + 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 + }; + + (key.to_string(), birthday, rescan) }; - let r = match lightclient.do_import_key(key.to_string(), birthday) { + let r = match lightclient.do_import_key(key, birthday) { Ok(r) => r.pretty(2), Err(e) => return format!("Error: {}", e), }; From e3266a00a6ccfec6786bc789b35296c6c3075caa Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Thu, 23 Jul 2020 11:01:14 -0700 Subject: [PATCH 11/21] 1.4.0 --- Cargo.lock | 2 +- cli/Cargo.toml | 2 +- cli/src/version.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2635965..a460287 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2504,7 +2504,7 @@ dependencies = [ [[package]] name = "zecwallet-cli" -version = "1.3.3" +version = "1.4.0" dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0ea5cc3..f275e9b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zecwallet-cli" -version = "1.3.3" +version = "1.4.0" edition = "2018" [dependencies] diff --git a/cli/src/version.rs b/cli/src/version.rs index f6cbe38..e9d11c2 100644 --- a/cli/src/version.rs +++ b/cli/src/version.rs @@ -1 +1 @@ -pub const VERSION:&str = "1.3.3"; \ No newline at end of file +pub const VERSION:&str = "1.4.0"; \ No newline at end of file From 5675f5b3591bf6725e6a66be02c1144c2ea4232d Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Mon, 27 Jul 2020 09:46:11 -0700 Subject: [PATCH 12/21] Add spendable to notes listing --- lib/src/lightclient.rs | 3 +++ lib/src/lightwallet.rs | 13 +++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 622c019..6d440bc 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -817,6 +817,8 @@ impl LightClient { let mut unspent_notes: Vec = vec![]; let mut spent_notes : Vec = vec![]; let mut pending_notes: Vec = vec![]; + + let anchor_height: i32 = self.wallet.read().unwrap().get_anchor_height() as i32; { // Collect Sapling notes @@ -834,6 +836,7 @@ impl LightClient { "value" => nd.note.value, "is_change" => nd.is_change, "address" => LightWallet::note_address(self.config.hrp_sapling_address(), nd), + "spendable" => wtx.block <= anchor_height && nd.spent.is_none() && nd.unconfirmed_spent.is_none(), "spent" => nd.spent.map(|spent_txid| format!("{}", spent_txid)), "spent_at_height" => nd.spent_at_height.map(|h| format!("{}", h)), "unconfirmed_spent" => nd.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 15e3f11..3a8e4b1 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -733,6 +733,14 @@ 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 memo_str(memo: &Option) -> Option { match memo { Some(memo) => { @@ -1003,10 +1011,7 @@ impl LightWallet { } pub fn spendable_zbalance(&self, addr: Option) -> u64 { - let anchor_height = match self.get_target_height_and_anchor_offset() { - Some((height, anchor_offset)) => height - anchor_offset as u32 - 1, - None => return 0, - }; + let anchor_height = self.get_anchor_height(); self.txs .read() From 22bde5a404694a6d121d439492ed1fcce361225a Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Tue, 28 Jul 2020 09:52:53 -0700 Subject: [PATCH 13/21] Interpret hex memos properly. Fixes #38 --- lib/src/lightclient.rs | 11 +++++++++-- lib/src/lightwallet.rs | 30 +++++++++++++----------------- lib/src/lightwallet/tests.rs | 10 ++++++++++ lib/src/lightwallet/utils.rs | 23 +++++++++++++++++++++++ 4 files changed, 55 insertions(+), 19 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 6d440bc..b37189b 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -1015,11 +1015,18 @@ impl LightClient { // Collect outgoing metadata let outgoing_json = wtx.outgoing_metadata.iter() - .map(|om| - object!{ + .map(|om| { + let mut o = object!{ "address" => om.address.clone(), "value" => om.value, "memo" => LightWallet::memo_str(&Some(om.memo.clone())), + }; + + if include_memo_hex { + o.insert("memohex", hex::encode(om.memo.as_bytes())).unwrap(); + } + + return o; }).collect::>(); object! { diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 3a8e4b1..9c602b8 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -2066,22 +2066,12 @@ impl LightWallet { Some(s) => { // If the string starts with an "0x", and contains only hex chars ([a-f0-9]+) then // interpret it as a hex - let s_bytes = if s.to_lowercase().starts_with("0x") { - match hex::decode(&s[2..s.len()]) { - Ok(data) => data, - Err(_) => Vec::from(s.as_bytes()) - } - } else { - Vec::from(s.as_bytes()) - }; - - match Memo::from_bytes(&s_bytes) { - None => { - let e = format!("Error creating output. Memo {:?} is too long", s); + match utils::interpret_memo_string(&s) { + Ok(m) => Some(m), + Err(e) => { error!("{}", e); return Err(e); - }, - Some(m) => Some(m) + } } } }; @@ -2154,10 +2144,16 @@ impl LightWallet { None => Memo::default(), Some(s) => { // If the address is not a z-address, then drop the memo - if LightWallet::is_shielded_address(&addr.to_string(), &self.config) { - Memo::from_bytes(s.as_bytes()).unwrap() - } else { + if !LightWallet::is_shielded_address(&addr.to_string(), &self.config) { Memo::default() + } else { + match utils::interpret_memo_string(s) { + Ok(m) => m, + Err(e) => { + error!("{}", e); + Memo::default() + } + } } } }, diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index c74bf25..d8ec8f7 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -1412,6 +1412,16 @@ fn test_z_incoming_hex_memo() { let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); + // Make sure it is in the mempool properly + { + let mempool = wallet.mempool_txs.read().unwrap(); + + let wtx = mempool.get(&sent_txid).unwrap(); + assert_eq!(wtx.outgoing_metadata.get(0).unwrap().address, my_address); + assert_eq!(wtx.outgoing_metadata.get(0).unwrap().value, AMOUNT1 - fee); + assert_eq!(wtx.outgoing_metadata.get(0).unwrap().memo.to_utf8().unwrap().unwrap(), orig_memo); + } + // Add it to a block let mut cb3 = FakeCompactBlock::new(2, block_hash); cb3.add_tx(&sent_tx); diff --git a/lib/src/lightwallet/utils.rs b/lib/src/lightwallet/utils.rs index 1ece11e..7fd464c 100644 --- a/lib/src/lightwallet/utils.rs +++ b/lib/src/lightwallet/utils.rs @@ -1,5 +1,6 @@ use std::io::{self, Read, Write}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use zcash_primitives::note_encryption::Memo; pub fn read_string(mut reader: R) -> io::Result { // Strings are written as len + bytes @@ -18,4 +19,26 @@ pub fn write_string(mut writer: W, s: &String) -> io::Result<()> { // Strings are written as len + utf8 writer.write_u64::(s.as_bytes().len() as u64)?; writer.write_all(s.as_bytes()) +} + +// Interpret a string or hex-encoded memo, and return a Memo object +pub fn interpret_memo_string(memo_str: &String) -> Result { + // If the string starts with an "0x", and contains only hex chars ([a-f0-9]+) then + // interpret it as a hex + let s_bytes = if memo_str.to_lowercase().starts_with("0x") { + match hex::decode(&memo_str[2..memo_str.len()]) { + Ok(data) => data, + Err(_) => Vec::from(memo_str.as_bytes()) + } + } else { + Vec::from(memo_str.as_bytes()) + }; + + match Memo::from_bytes(&s_bytes) { + None => { + let e = format!("Error creating output. Memo {:?} is too long", memo_str); + return Err(e); + }, + Some(m) => Ok(m) + } } \ No newline at end of file From 5a885adc15fb1aeed4ca16b87286719d45893a7a Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Wed, 29 Jul 2020 09:59:22 -0700 Subject: [PATCH 14/21] Grab sync lock when sending to prevent anchor changes. Fixes #40 --- lib/src/lightclient.rs | 14 +++++++++----- lib/src/lightwallet.rs | 12 ++++++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 6d440bc..9d5419c 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -1483,11 +1483,15 @@ impl LightClient { info!("Creating transaction"); - let rawtx = self.wallet.write().unwrap().send_to_address( - u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), - &self.sapling_spend, &self.sapling_output, - addrs - ); + let rawtx = { + let _lock = self.sync_lock.lock().unwrap(); + + self.wallet.write().unwrap().send_to_address( + u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), + &self.sapling_spend, &self.sapling_output, + addrs + ) + }; match rawtx { Ok(txbytes) => broadcast_raw_tx(&self.get_server_uri(), txbytes), diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 3a8e4b1..d5a957d 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -1946,7 +1946,9 @@ impl LightWallet { // Select notes to cover the target value println!("{}: Selecting notes", now() - start_time); let target_value = Amount::from_u64(total_value).unwrap() + DEFAULT_FEE ; - let notes: Vec<_> = self.txs.read().unwrap().iter() + + // 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)| { @@ -1960,7 +1962,13 @@ impl LightWallet { .and_then(|zk| zk.extsk.clone()); SpendableNote::from(txid, note, anchor_offset, &extsk) } - }) + }).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) { From b171d28586b00ed7c94b05eee61606eb9d8bd475 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 31 Jul 2020 18:53:16 -0700 Subject: [PATCH 15/21] Add to mempool only if broadcast works. Fixes #43 --- lib/src/lightclient.rs | 10 ++- lib/src/lightwallet.rs | 20 +++--- lib/src/lightwallet/tests.rs | 114 +++++++++++++++++------------------ 3 files changed, 74 insertions(+), 70 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 20f23ed..1121e7f 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -1490,20 +1490,18 @@ impl LightClient { info!("Creating transaction"); - let rawtx = { + let result = { let _lock = self.sync_lock.lock().unwrap(); self.wallet.write().unwrap().send_to_address( u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), &self.sapling_spend, &self.sapling_output, - addrs + addrs, + |txbytes| broadcast_raw_tx(&self.get_server_uri(), txbytes) ) }; - match rawtx { - Ok(txbytes) => broadcast_raw_tx(&self.get_server_uri(), txbytes), - Err(e) => Err(format!("Error: No Tx to broadcast. Error was: {}", e)) - } + result.map(|(txid, _)| txid) } } diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index b383fa9..9212074 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -1892,13 +1892,16 @@ impl LightWallet { }); } - pub fn send_to_address( + pub fn send_to_address ( &self, consensus_branch_id: u32, spend_params: &[u8], output_params: &[u8], - tos: Vec<(&str, u64, Option)> - ) -> Result, String> { + tos: Vec<(&str, u64, Option)>, + broadcast_fn: F + ) -> Result<(String, Vec), String> + where F: Fn(Box<[u8]>) -> Result + { if !self.unlocked { return Err("Cannot spend while wallet is locked".to_string()); } @@ -2116,6 +2119,12 @@ impl LightWallet { println!("{}: Transaction created", now() - start_time); println!("Transaction ID: {}", tx.txid()); + // Create the TX bytes + let mut raw_tx = vec![]; + tx.write(&mut raw_tx).unwrap(); + + let txid = broadcast_fn(raw_tx.clone().into_boxed_slice())?; + // Mark notes as spent. { // Mark sapling notes as unconfirmed spent @@ -2181,10 +2190,7 @@ impl LightWallet { } } - // Return the encoded transaction, so the caller can send it. - let mut raw_tx = vec![]; - tx.write(&mut raw_tx).unwrap(); - Ok(raw_tx.into_boxed_slice()) + Ok((txid, raw_tx)) } // After some blocks have been mined, we need to remove the Txns from the mempool_tx structure diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index d8ec8f7..0f05f8c 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -743,8 +743,8 @@ fn test_witness_updates() { 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, - vec![(&ext_address, AMOUNT_SENT, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_address, AMOUNT_SENT, None)], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -807,8 +807,8 @@ fn test_z_spend_to_z() { } // Create a tx and send to address - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -923,8 +923,8 @@ fn test_self_txns_ttoz_withmemo() { let (ss, so) =get_sapling_params().unwrap(); // 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, - vec![(&zaddr, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&zaddr, AMOUNT_SENT, Some(outgoing_memo.clone()))], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -981,8 +981,8 @@ fn test_self_txns_ttoz_nomemo() { let (ss, so) =get_sapling_params().unwrap(); // 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, - vec![(&zaddr, AMOUNT_SENT, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&zaddr, AMOUNT_SENT, None)], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -1017,8 +1017,8 @@ fn test_self_txns_ztoz() { 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, - vec![(&zaddr2, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&zaddr2, AMOUNT_SENT, Some(outgoing_memo.clone()))], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -1037,8 +1037,8 @@ fn test_self_txns_ztoz() { } // Another self tx, this time without a memo - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&zaddr2, AMOUNT_SENT, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&zaddr2, AMOUNT_SENT, None)], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -1071,8 +1071,8 @@ fn test_multi_z() { 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, - vec![(&zaddr2, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&zaddr2, AMOUNT_SENT, Some(outgoing_memo.clone()))], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -1126,8 +1126,8 @@ fn test_multi_z() { 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, - vec![(&taddr, amount_all, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr, amount_all, None)], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_ext_txid = sent_tx.txid(); @@ -1168,8 +1168,8 @@ fn test_z_spend_to_taddr() { const AMOUNT_SENT: u64 = 30; let fee: u64 = DEFAULT_FEE.try_into().unwrap(); - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&taddr, AMOUNT_SENT, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr, AMOUNT_SENT, None)], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -1216,8 +1216,8 @@ fn test_z_spend_to_taddr() { } // Create a new Tx, but this time with a memo. - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&taddr, AMOUNT_SENT, Some("T address memo".to_string()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr, AMOUNT_SENT, Some("T address memo".to_string()))], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid2 = sent_tx.txid(); @@ -1291,8 +1291,8 @@ fn test_t_spend_to_z() { let (ss, so) =get_sapling_params().unwrap(); // 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, - vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -1365,8 +1365,8 @@ fn test_z_incoming_memo() { 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, - vec![(&my_address, AMOUNT1 - fee, Some(memo.clone()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&my_address, AMOUNT1 - fee, Some(memo.clone()))], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -1407,8 +1407,8 @@ fn test_z_incoming_hex_memo() { 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, - vec![(&my_address, AMOUNT1 - fee, Some(memo.clone()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&my_address, AMOUNT1 - fee, Some(memo.clone()))], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -1460,8 +1460,8 @@ fn test_add_new_zt_hd_after_incoming() { assert_eq!(wallet.zkeys.read().unwrap().len(), 6); // Starts with 1+5 addresses // Create a tx and send to the last address - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&my_address, AMOUNT1 - fee, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&my_address, AMOUNT1 - fee, None)], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); // Add it to a block @@ -1503,8 +1503,8 @@ fn test_z_to_t_withinwallet() { 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, - vec![(&taddr, AMOUNT_SENT, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr, AMOUNT_SENT, None)], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -1563,8 +1563,8 @@ fn test_multi_t() { 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, - vec![(&taddr2, AMOUNT_SENT1, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr2, AMOUNT_SENT1, None)], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid1 = sent_tx.txid(); @@ -1607,8 +1607,8 @@ fn test_multi_t() { 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, - vec![(&taddr3, AMOUNT_SENT2, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr3, AMOUNT_SENT2, None)], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid2 = sent_tx.txid(); @@ -1644,8 +1644,8 @@ fn test_multi_t() { 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, - vec![(&ext_address, AMOUNT_SENT_EXT, Some(outgoing_memo.clone()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_address, AMOUNT_SENT_EXT, Some(outgoing_memo.clone()))], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid3 = sent_tx.txid(); @@ -1702,7 +1702,7 @@ fn test_multi_spends() { (taddr2.as_str(), TAMOUNT2, None), (taddr3.as_str(), TAMOUNT3, None) ]; - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, tos).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, tos, |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -1774,7 +1774,7 @@ fn test_multi_spends() { 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 (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, tos, |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid2 = sent_tx.txid(); @@ -1827,17 +1827,17 @@ fn test_bad_send() { // Bad address let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&"badaddress", 10, None)]); + vec![(&"badaddress", 10, None)], |_| Ok(' '.to_string())); 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)]); + vec![(&ext_taddr, AMOUNT1 + 10, None)], |_| Ok(' '.to_string())); assert!(raw_tx.err().unwrap().contains("Insufficient verified funds")); // No addresses - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, vec![]); + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, vec![], |_| Ok(' '.to_string())); assert!(raw_tx.err().unwrap().contains("at least one")); } @@ -1858,7 +1858,7 @@ fn test_duplicate_outputs() { let raw_tx = wallet.send_to_address(branch_id, &ss, &so, vec![(&ext_taddr, 100, Some("First memo".to_string())), (&ext_taddr, 0, Some("Second memo".to_string())), - (&ext_taddr, 0, Some("Third memo".to_string()))]); + (&ext_taddr, 0, Some("Third memo".to_string()))], |_| Ok(' '.to_string())); assert!(raw_tx.is_ok()); } @@ -1871,7 +1871,7 @@ fn test_bad_params() { let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); // Bad params let _ = wallet.send_to_address(branch_id, &[], &[], - vec![(&ext_taddr, 10, None)]); + vec![(&ext_taddr, 10, None)], |_| Ok(' '.to_string())); } /// Test helper to add blocks @@ -1907,8 +1907,8 @@ fn test_z_mempool_expiry() { 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, - vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -2024,8 +2024,8 @@ fn test_rollback() { // Create a tx and send to address const AMOUNT_SENT: u64 = 30000; let fee: u64 = DEFAULT_FEE.try_into().unwrap(); - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&taddr, AMOUNT_SENT, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr, AMOUNT_SENT, None)], |_| Ok(' '.to_string())).unwrap(); let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); @@ -2497,8 +2497,8 @@ fn test_encrypted_zreceive() { 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, - vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))], |_| Ok(' '.to_string())).unwrap(); // Now that we have the transaction, we'll encrypt the wallet wallet.encrypt(password.clone()).unwrap(); @@ -2541,7 +2541,7 @@ fn test_encrypted_zreceive() { // Trying to spend from a locked wallet is an error assert!(wallet.send_to_address(branch_id, &ss, &so, - vec![(&ext_address, AMOUNT_SENT, None)]).is_err()); + vec![(&ext_address, AMOUNT_SENT, None)], |_| Ok(' '.to_string())).is_err()); // unlock the wallet so we can spend to the second z address wallet.unlock(password.clone()).unwrap(); @@ -2551,8 +2551,8 @@ fn test_encrypted_zreceive() { const ZAMOUNT2:u64 = 30; let outgoing_memo2 = "Outgoing Memo2".to_string(); - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&zaddr2, ZAMOUNT2, Some(outgoing_memo2.clone()))]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&zaddr2, ZAMOUNT2, Some(outgoing_memo2.clone()))], |_| Ok(' '.to_string())).unwrap(); // Now lock the wallet again wallet.lock().unwrap(); @@ -2606,8 +2606,8 @@ fn test_encrypted_treceive() { const AMOUNT_SENT: u64 = 30; let fee: u64 = DEFAULT_FEE.try_into().unwrap(); - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&taddr, AMOUNT_SENT, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr, AMOUNT_SENT, None)], |_| Ok(' '.to_string())).unwrap(); // Now that we have the transaction, we'll encrypt the wallet wallet.encrypt(password.clone()).unwrap(); @@ -2643,7 +2643,7 @@ fn test_encrypted_treceive() { // Trying to spend from a locked wallet is an error assert!(wallet.send_to_address(branch_id, &ss, &so, - vec![(&taddr, AMOUNT_SENT, None)]).is_err()); + vec![(&taddr, AMOUNT_SENT, None)], |_| Ok(' '.to_string())).is_err()); // unlock the wallet so we can spend to the second z address wallet.unlock(password.clone()).unwrap(); @@ -2652,8 +2652,8 @@ fn test_encrypted_treceive() { let taddr2 = wallet.add_taddr(); const TAMOUNT2:u64 = 50; - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&taddr2, TAMOUNT2, None)]).unwrap(); + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr2, TAMOUNT2, None)], |_| Ok(' '.to_string())).unwrap(); // Now lock the wallet again wallet.lock().unwrap(); From a087a4411f0d5d6e7404b1a91d455dc13a04b508 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Tue, 4 Aug 2020 10:53:40 -0700 Subject: [PATCH 16/21] "spendable" reflects if we have the spending key. Fixes #44 --- lib/src/lightclient.rs | 21 +++++++++++++++++---- lib/src/lightwallet.rs | 21 +++++++++++++++++---- lib/src/lightwallet/tests.rs | 12 ++++++++++++ lib/src/lightwallet/walletzkey.rs | 4 ++++ 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 1121e7f..f3f2bbb 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -6,7 +6,7 @@ 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::*; @@ -821,22 +821,35 @@ impl LightClient { 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 = 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), - "spendable" => wtx.block <= anchor_height && nd.spent.is_none() && nd.unconfirmed_spent.is_none(), + "address" => address, + "spendable" => spendable, "spent" => nd.spent.map(|spent_txid| format!("{}", spent_txid)), "spent_at_height" => nd.spent_at_height.map(|h| format!("{}", h)), "unconfirmed_spent" => nd.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)), diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 9212074..ba5762a 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -1024,10 +1024,7 @@ impl LightWallet { .filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none()) .filter(|nd| { // Check to see if we have this note's spending key. - match self.zkeys.read().unwrap().iter().find(|zk| zk.extfvk == nd.extfvk) { - Some(zk) => zk.keytype == WalletZKeyType::HdKey || zk.keytype == WalletZKeyType::ImportedSpendingKey, - _ => false - } + self.have_spendingkey_for_extfvk(&nd.extfvk) }) .filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it. match addr.clone() { @@ -1048,6 +1045,22 @@ impl LightWallet { .sum::() } + 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(); diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index 0f05f8c..ef16d4e 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -813,6 +813,8 @@ fn test_z_spend_to_z() { let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); + assert_eq!(wallet.have_spending_key_for_zaddress(wallet.get_all_zaddresses().get(0).unwrap()), true); + // Now, the note should be unconfirmed spent { let txs = wallet.txs.read().unwrap(); @@ -2363,6 +2365,8 @@ fn test_import_vk() { assert_eq!(wallet.zkeys.read().unwrap()[1].keytype, WalletZKeyType::ImportedViewKey); assert_eq!(wallet.zkeys.read().unwrap()[1].hdkey_num, None); + assert_eq!(wallet.have_spending_key_for_zaddress(&zaddr.to_string()), false); + // Importing it again should fail assert!(wallet.add_imported_sk(viewkey.to_string(), 0).starts_with("Error")); assert_eq!(wallet.get_all_zaddresses().len(), 2); @@ -2411,6 +2415,8 @@ fn test_import_sk_upgrade_vk() { assert_eq!(wallet.zkeys.read().unwrap()[1].hdkey_num, None); assert!(wallet.zkeys.read().unwrap()[1].extsk.is_none()); + assert_eq!(wallet.have_spending_key_for_zaddress(&zaddr.to_string()), false); + // Importing it again should fail because it already exists assert!(wallet.add_imported_sk(viewkey.to_string(), 0).starts_with("Error")); assert_eq!(wallet.get_all_zaddresses().len(), 2); @@ -2431,6 +2437,8 @@ fn test_import_sk_upgrade_vk() { assert_eq!(wallet.zkeys.read().unwrap()[1].keytype, WalletZKeyType::ImportedSpendingKey); assert_eq!(wallet.zkeys.read().unwrap()[1].hdkey_num, None); assert!(wallet.zkeys.read().unwrap()[1].extsk.is_some()); + + assert_eq!(wallet.have_spending_key_for_zaddress(&zaddr.to_string()), true); } #[test] @@ -2500,9 +2508,13 @@ fn test_encrypted_zreceive() { let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))], |_| Ok(' '.to_string())).unwrap(); + assert_eq!(wallet.have_spending_key_for_zaddress(wallet.get_all_zaddresses().get(0).unwrap()), true); + // Now that we have the transaction, we'll encrypt the wallet wallet.encrypt(password.clone()).unwrap(); + assert_eq!(wallet.have_spending_key_for_zaddress(wallet.get_all_zaddresses().get(0).unwrap()), true); + // Scan the tx and make sure it gets added let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); let sent_txid = sent_tx.txid(); diff --git a/lib/src/lightwallet/walletzkey.rs b/lib/src/lightwallet/walletzkey.rs index d6c3a1f..3c97c49 100644 --- a/lib/src/lightwallet/walletzkey.rs +++ b/lib/src/lightwallet/walletzkey.rs @@ -102,6 +102,10 @@ impl WalletZKey { } } + pub fn have_spending_key(&self) -> bool { + self.extsk.is_some() || self.enc_key.is_some() || self.hdkey_num.is_some() + } + fn serialized_version() -> u8 { return 1; } From fb1cf996e594896543fa6dd7eef334e5827b2047 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Wed, 5 Aug 2020 10:00:12 -0700 Subject: [PATCH 17/21] Add broadcast failure test --- lib/src/lightwallet/tests.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index ef16d4e..eef222d 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -1836,12 +1836,18 @@ fn test_bad_send() { let raw_tx = wallet.send_to_address(branch_id, &ss, &so, vec![(&ext_taddr, AMOUNT1 + 10, None)], |_| Ok(' '.to_string())); assert!(raw_tx.err().unwrap().contains("Insufficient verified funds")); - + assert_eq!(wallet.mempool_txs.read().unwrap().len(), 0); // No addresses let raw_tx = wallet.send_to_address(branch_id, &ss, &so, vec![], |_| Ok(' '.to_string())); assert!(raw_tx.err().unwrap().contains("at least one")); + assert_eq!(wallet.mempool_txs.read().unwrap().len(), 0); + // Broadcast error + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_taddr, 10, None)], |_| Err("broadcast failed".to_string())); + assert!(raw_tx.err().unwrap().contains("broadcast failed")); + assert_eq!(wallet.mempool_txs.read().unwrap().len(), 0); } #[test] @@ -1862,6 +1868,9 @@ fn test_duplicate_outputs() { (&ext_taddr, 0, Some("Second memo".to_string())), (&ext_taddr, 0, Some("Third memo".to_string()))], |_| Ok(' '.to_string())); assert!(raw_tx.is_ok()); + + // Make sure they are in the mempool + assert_eq!(wallet.mempool_txs.read().unwrap().len(), 1); } #[test] From 263ec19476ccb04232e67fcb2e4a9daed3b7a83c Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Thu, 6 Aug 2020 11:33:32 -0700 Subject: [PATCH 18/21] Add "unconfirmed_zbalance" to the balance command. Fixes #46 --- lib/src/lightclient.rs | 6 ++- lib/src/lightwallet.rs | 38 ++++++++++++++++++ lib/src/lightwallet/tests.rs | 77 ++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index f3f2bbb..f166e60 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -679,8 +679,9 @@ impl LightClient { object!{ "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())) + "verified_zbalance" => wallet.verified_zbalance(Some(zaddress.clone())), + "spendable_zbalance" => wallet.spendable_zbalance(Some(zaddress.clone())), + "unconfirmed_zbalance" => wallet.unconfirmed_zbalance(Some(zaddress.clone())) } }).collect::>(); @@ -699,6 +700,7 @@ impl LightClient { "zbalance" => wallet.zbalance(None), "verified_zbalance" => wallet.verified_zbalance(None), "spendable_zbalance" => wallet.spendable_zbalance(None), + "unconfirmed_zbalance" => wallet.unconfirmed_zbalance(None), "tbalance" => wallet.tbalance(None), "z_addresses" => z_addresses, "t_addresses" => t_addresses, diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index ba5762a..e4976aa 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -976,6 +976,44 @@ impl LightWallet { .sum::() } + pub fn unconfirmed_zbalance(&self, addr: Option) -> u64 { + let anchor_height = match self.get_target_height_and_anchor_offset() { + Some((height, anchor_offset)) => height - anchor_offset as u32 - 1, + None => return 0, + }; + + self.txs + .read() + .unwrap() + .values() + .map(|tx| { + 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( + self.config.hrp_sapling_address(), + &nd.extfvk.fvk.vk + .to_payment_address(nd.diversifier, &JUBJUB).unwrap() + ), + None => true + } + }) + .map(|nd| { + if tx.block as u32 <= anchor_height { + // If confirmed, then unconfirmed is 0 + 0 + } else { + // If confirmed but dont have anchor yet, it is unconfirmed + nd.note.value + } + }) + .sum::() + }) + .sum::() + } + pub fn verified_zbalance(&self, addr: Option) -> u64 { let anchor_height = match self.get_target_height_and_anchor_offset() { Some((height, anchor_offset)) => height - anchor_offset as u32 - 1, diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index eef222d..77e69e2 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -699,6 +699,7 @@ fn get_test_wallet(amount: u64) -> (LightWallet, TxId, BlockHash) { } assert_eq!(wallet.verified_zbalance(None), amount); + assert_eq!(wallet.unconfirmed_zbalance(None), 0); // Create a new block so that the note is now verified to be spent let cb2 = FakeCompactBlock::new(1, cb1.hash()); @@ -707,6 +708,82 @@ fn get_test_wallet(amount: u64) -> (LightWallet, TxId, BlockHash) { (wallet, txid1, cb2.hash()) } +#[test] +fn test_unconfirmed_txns() { + let config = LightClientConfig { + server: "0.0.0.0:0".parse().unwrap(), + chain_name: "test".to_string(), + sapling_activation_height: 0, + consensus_branch_id: "000000".to_string(), + anchor_offset: 5, // offset = 5 + data_dir: None, + }; + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + let amount = 50000; + let wallet = LightWallet::new(None, &config, 0).unwrap(); + + let mut block = FakeCompactBlock::new(0, BlockHash([0; 32])); + let (_, _txid) = block.add_tx_paying(wallet.zkeys.read().unwrap()[0].extfvk.clone(), amount); + wallet.scan_block(&block.as_bytes()).unwrap(); + + // Construct 5 blocks so that we can get started + for i in 0..5 { + block = FakeCompactBlock::new(1+i, block.hash()); + wallet.scan_block(&block.as_bytes()).unwrap(); + } + + // Make sure the starting balances are correct + assert_eq!(wallet.verified_zbalance(None), amount); + assert_eq!(wallet.unconfirmed_zbalance(None), 0); + + // Now spend some of the money, paying our own address + let zaddr1 = encode_payment_address(wallet.config.hrp_sapling_address(), &wallet.zkeys.read().unwrap().get(0).unwrap().zaddress); + let zaddr2 = wallet.add_zaddr(); + const AMOUNT_SENT: u64 = 50; + let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, + vec![(&zaddr2, AMOUNT_SENT, None)], |_| Ok(' '.to_string())).unwrap(); + + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + + block = FakeCompactBlock::new(6, block.hash()); + block.add_tx(&sent_tx); + wallet.scan_block(&block.as_bytes()).unwrap(); + + // pending tx + assert_eq!(wallet.unconfirmed_zbalance(Some(zaddr1.clone())), amount - AMOUNT_SENT - fee); + assert_eq!(wallet.verified_zbalance(Some(zaddr1.clone())), 0); + assert_eq!(wallet.spendable_zbalance(Some(zaddr1.clone())), 0); + + assert_eq!(wallet.unconfirmed_zbalance(None), amount - fee); + assert_eq!(wallet.verified_zbalance(None), 0); + assert_eq!(wallet.spendable_zbalance(None), 0); + + assert_eq!(wallet.unconfirmed_zbalance(Some(zaddr2.clone())), AMOUNT_SENT); + assert_eq!(wallet.verified_zbalance(Some(zaddr2.clone())), 0); + assert_eq!(wallet.spendable_zbalance(Some(zaddr2.clone())), 0); + + // Mine 5 blocks, so it becomes confirmed + for i in 0..5 { + block = FakeCompactBlock::new(7+i, block.hash()); + wallet.scan_block(&block.as_bytes()).unwrap(); + } + assert_eq!(wallet.unconfirmed_zbalance(Some(zaddr1.clone())), 0); + assert_eq!(wallet.verified_zbalance(Some(zaddr1.clone())), amount - AMOUNT_SENT - fee); + assert_eq!(wallet.spendable_zbalance(Some(zaddr1.clone())), amount - AMOUNT_SENT - fee); + + assert_eq!(wallet.unconfirmed_zbalance(None), 0); + assert_eq!(wallet.verified_zbalance(None), amount - fee); + assert_eq!(wallet.spendable_zbalance(None), amount - fee); + + assert_eq!(wallet.unconfirmed_zbalance(Some(zaddr2.clone())), 0); + assert_eq!(wallet.verified_zbalance(Some(zaddr2.clone())), AMOUNT_SENT); + assert_eq!(wallet.spendable_zbalance(Some(zaddr2.clone())), AMOUNT_SENT); +} + #[test] fn test_witness_updates() { const AMOUNT1: u64 = 50000; From 6187156936aa9f66ce70fb80fc9aeb188b34e98e Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Thu, 6 Aug 2020 15:28:22 -0700 Subject: [PATCH 19/21] v1.4.1 --- Cargo.lock | 2 +- cli/Cargo.toml | 2 +- cli/src/version.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a460287..1196a24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2504,7 +2504,7 @@ dependencies = [ [[package]] name = "zecwallet-cli" -version = "1.4.0" +version = "1.4.1" dependencies = [ "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f275e9b..7f90298 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zecwallet-cli" -version = "1.4.0" +version = "1.4.1" edition = "2018" [dependencies] diff --git a/cli/src/version.rs b/cli/src/version.rs index e9d11c2..757f2f3 100644 --- a/cli/src/version.rs +++ b/cli/src/version.rs @@ -1 +1 @@ -pub const VERSION:&str = "1.4.0"; \ No newline at end of file +pub const VERSION:&str = "1.4.1"; From 06a268c2bd6412283100acd81c8f012608154c5d Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Fri, 7 Aug 2020 10:11:55 -0700 Subject: [PATCH 20/21] "unconfirmed" -> "unverified" --- lib/src/lightclient.rs | 4 ++-- lib/src/lightwallet.rs | 6 +++++- lib/src/lightwallet/tests.rs | 16 ++++++++-------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index f166e60..e669d45 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -681,7 +681,7 @@ impl LightClient { "zbalance" => wallet.zbalance(Some(zaddress.clone())), "verified_zbalance" => wallet.verified_zbalance(Some(zaddress.clone())), "spendable_zbalance" => wallet.spendable_zbalance(Some(zaddress.clone())), - "unconfirmed_zbalance" => wallet.unconfirmed_zbalance(Some(zaddress.clone())) + "unverified_zbalance" => wallet.unverified_zbalance(Some(zaddress.clone())) } }).collect::>(); @@ -700,7 +700,7 @@ impl LightClient { "zbalance" => wallet.zbalance(None), "verified_zbalance" => wallet.verified_zbalance(None), "spendable_zbalance" => wallet.spendable_zbalance(None), - "unconfirmed_zbalance" => wallet.unconfirmed_zbalance(None), + "unverified_zbalance" => wallet.unverified_zbalance(None), "tbalance" => wallet.tbalance(None), "z_addresses" => z_addresses, "t_addresses" => t_addresses, diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index e4976aa..bc0b339 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -976,7 +976,7 @@ impl LightWallet { .sum::() } - pub fn unconfirmed_zbalance(&self, addr: Option) -> u64 { + pub fn unverified_zbalance(&self, addr: Option) -> u64 { let anchor_height = match self.get_target_height_and_anchor_offset() { Some((height, anchor_offset)) => height - anchor_offset as u32 - 1, None => return 0, @@ -990,6 +990,10 @@ impl LightWallet { 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( diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index 77e69e2..986b909 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -699,7 +699,7 @@ fn get_test_wallet(amount: u64) -> (LightWallet, TxId, BlockHash) { } assert_eq!(wallet.verified_zbalance(None), amount); - assert_eq!(wallet.unconfirmed_zbalance(None), 0); + assert_eq!(wallet.unverified_zbalance(None), 0); // Create a new block so that the note is now verified to be spent let cb2 = FakeCompactBlock::new(1, cb1.hash()); @@ -738,7 +738,7 @@ fn test_unconfirmed_txns() { // Make sure the starting balances are correct assert_eq!(wallet.verified_zbalance(None), amount); - assert_eq!(wallet.unconfirmed_zbalance(None), 0); + assert_eq!(wallet.unverified_zbalance(None), 0); // Now spend some of the money, paying our own address let zaddr1 = encode_payment_address(wallet.config.hrp_sapling_address(), &wallet.zkeys.read().unwrap().get(0).unwrap().zaddress); @@ -754,15 +754,15 @@ fn test_unconfirmed_txns() { wallet.scan_block(&block.as_bytes()).unwrap(); // pending tx - assert_eq!(wallet.unconfirmed_zbalance(Some(zaddr1.clone())), amount - AMOUNT_SENT - fee); + assert_eq!(wallet.unverified_zbalance(Some(zaddr1.clone())), amount - AMOUNT_SENT - fee); assert_eq!(wallet.verified_zbalance(Some(zaddr1.clone())), 0); assert_eq!(wallet.spendable_zbalance(Some(zaddr1.clone())), 0); - assert_eq!(wallet.unconfirmed_zbalance(None), amount - fee); + assert_eq!(wallet.unverified_zbalance(None), amount - fee); assert_eq!(wallet.verified_zbalance(None), 0); assert_eq!(wallet.spendable_zbalance(None), 0); - assert_eq!(wallet.unconfirmed_zbalance(Some(zaddr2.clone())), AMOUNT_SENT); + assert_eq!(wallet.unverified_zbalance(Some(zaddr2.clone())), AMOUNT_SENT); assert_eq!(wallet.verified_zbalance(Some(zaddr2.clone())), 0); assert_eq!(wallet.spendable_zbalance(Some(zaddr2.clone())), 0); @@ -771,15 +771,15 @@ fn test_unconfirmed_txns() { block = FakeCompactBlock::new(7+i, block.hash()); wallet.scan_block(&block.as_bytes()).unwrap(); } - assert_eq!(wallet.unconfirmed_zbalance(Some(zaddr1.clone())), 0); + assert_eq!(wallet.unverified_zbalance(Some(zaddr1.clone())), 0); assert_eq!(wallet.verified_zbalance(Some(zaddr1.clone())), amount - AMOUNT_SENT - fee); assert_eq!(wallet.spendable_zbalance(Some(zaddr1.clone())), amount - AMOUNT_SENT - fee); - assert_eq!(wallet.unconfirmed_zbalance(None), 0); + assert_eq!(wallet.unverified_zbalance(None), 0); assert_eq!(wallet.verified_zbalance(None), amount - fee); assert_eq!(wallet.spendable_zbalance(None), amount - fee); - assert_eq!(wallet.unconfirmed_zbalance(Some(zaddr2.clone())), 0); + assert_eq!(wallet.unverified_zbalance(Some(zaddr2.clone())), 0); assert_eq!(wallet.verified_zbalance(Some(zaddr2.clone())), AMOUNT_SENT); assert_eq!(wallet.spendable_zbalance(Some(zaddr2.clone())), AMOUNT_SENT); } From 6c0a672d6643b82b336160da5d3c05cbd2167967 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Tue, 11 Aug 2020 12:09:25 -0700 Subject: [PATCH 21/21] Fix android crash --- lib/src/lightclient.rs | 43 +++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index e669d45..fa96ce4 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -146,6 +146,9 @@ impl LightClientConfig { zcash_data_location = dirs::data_dir().expect("Couldn't determine app data directory!"); zcash_data_location.push("Zcash"); } else { + if dirs::home_dir().is_none() { + info!("Couldn't determine home dir!"); + } zcash_data_location = dirs::home_dir().expect("Couldn't determine home directory!"); zcash_data_location.push(".zcash"); }; @@ -174,6 +177,10 @@ impl LightClientConfig { } pub fn get_zcash_params_path(&self) -> io::Result> { + if dirs::home_dir().is_none() { + return Err(io::Error::new(io::ErrorKind::InvalidData, "Couldn't determine Home Dir")); + } + let mut zcash_params = self.get_zcash_data_path().into_path_buf(); zcash_params.push(".."); if cfg!(target_os="macos") || cfg!(target_os="windows") { @@ -375,24 +382,26 @@ impl LightClient { 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) + // Ensure that the sapling params are stored on disk properly as well. Only on desktop + if cfg!(all(not(target_os="ios"), not(target_os="android"))) { + 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); } - }, - Err(e) => { - eprintln!("{}", e); - } - }; + }; + } Ok(()) }