From 710a580f253c1a6b9cb58698a963db9ebbb29579 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Mon, 21 Oct 2019 17:18:52 -0700 Subject: [PATCH 1/7] Add rtests for ecieve t/z while encrypted --- lib/src/lightwallet.rs | 219 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 218 insertions(+), 1 deletion(-) diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 0c4dbed..14a3aed 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -2232,7 +2232,7 @@ pub mod tests { let fee: u64 = DEFAULT_FEE.try_into().unwrap(); let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) =get_sapling_params().unwrap(); + let (ss, so) = get_sapling_params().unwrap(); // Create a tx and send to address let raw_tx = wallet.send_to_address(branch_id, &ss, &so, @@ -3270,4 +3270,221 @@ pub mod tests { let _ = add_blocks(&wallet, 3, 2, prev_hash).unwrap(); assert_eq!(wallet.blocks.read().unwrap().len(), 5); } + + #[test] + fn test_encrypted_zreceive() { + const AMOUNT1: u64 = 50000; + let password: String = "password".to_string(); + + let (mut wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); + + 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 outgoing_memo = "Outgoing Memo".to_string(); + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + // Create a tx and send to address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); + + // Now that we have the transaction, we'll encrypt the wallet + wallet.encrypt(password.clone()).unwrap(); + + // Scan the tx and make sure it gets added + 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(); + + // Now, full scan the Tx, which should populate the Outgoing Meta data + wallet.scan_full_tx(&sent_tx, 2, 0); + + // Now this new Spent tx should be in, so the note should be marked confirmed spent + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + + // The sent tx should generate change + assert_eq!(txs[&sent_txid].notes.len(), 1); + assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, None); + assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); + + // Outgoing Metadata + assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); + + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); + + assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, ext_address); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); + } + + // 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()); + + // unlock the wallet so we can spend to the second z address + wallet.unlock(password.clone()).unwrap(); + + // Second z address + let zaddr2 = wallet.add_zaddr(); + 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(); + + // Now lock the wallet again + wallet.lock().unwrap(); + + let sent_tx2 = Transaction::read(&raw_tx[..]).unwrap(); + let txid2 = sent_tx2.txid(); + + let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); + cb4.add_tx(&sent_tx2); + wallet.scan_block(&cb4.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx2, 3, 0); + + { + let txs = wallet.txs.read().unwrap(); + let prev_change_value = AMOUNT1 - AMOUNT_SENT - fee; + + // Change note from prev transaction is spent + assert_eq!(txs[&sent_txid].notes[0].note.value, prev_change_value); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, Some(txid2)); + + // New change note. So find it. + let change_note = txs[&txid2].notes.iter().find(|n| n.is_change).unwrap(); + + // New incoming tx is present + assert_eq!(change_note.note.value, prev_change_value - (ZAMOUNT2+fee)); + assert_eq!(change_note.spent, None); + assert_eq!(change_note.unconfirmed_spent, None); + + // Find zaddr2 + let zaddr2_note = txs[&txid2].notes.iter().find(|n| n.note.value == ZAMOUNT2).unwrap(); + assert_eq!(zaddr2_note.account, 1); + assert_eq!(zaddr2_note.is_change, false); + assert_eq!(zaddr2_note.spent, None); + assert_eq!(zaddr2_note.unconfirmed_spent, None); + assert_eq!(LightWallet::memo_str(&zaddr2_note.memo), Some(outgoing_memo2)); + } + } + + + #[test] + fn test_encrypted_treceive() { + const AMOUNT1: u64 = 50000; + let password: String = "password".to_string(); + let (mut wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); + 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(); + + // Now that we have the transaction, we'll encrypt the wallet + wallet.encrypt(password.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); + + // Now this new Spent tx should be in, so the note should be marked confirmed spent + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + + // The sent tx should generate change + assert_eq!(txs[&sent_txid].notes.len(), 1); + assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, None); + assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); + + // Outgoing Metadata + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, taddr); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT); + assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); + } + + // 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()); + + // unlock the wallet so we can spend to the second z address + wallet.unlock(password.clone()).unwrap(); + + // Second z address + 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(); + + // Now lock the wallet again + wallet.lock().unwrap(); + + let sent_tx2 = Transaction::read(&raw_tx[..]).unwrap(); + let txid2 = sent_tx2.txid(); + + let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); + cb4.add_tx(&sent_tx2); + wallet.scan_block(&cb4.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx2, 3, 0); + + { + let txs = wallet.txs.read().unwrap(); + let prev_change_value = AMOUNT1 - AMOUNT_SENT - fee; + + // Change note from prev transaction is spent + assert_eq!(txs[&sent_txid].notes[0].note.value, prev_change_value); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, Some(txid2)); + + // New change note. So find it. + let change_note = txs[&txid2].notes.iter().find(|n| n.is_change).unwrap(); + + // New incoming tx is present + assert_eq!(change_note.note.value, prev_change_value - (TAMOUNT2+fee)); + assert_eq!(change_note.spent, None); + assert_eq!(change_note.unconfirmed_spent, None); + + // Find taddr2 + let utxo2 = txs[&txid2].utxos.iter().find(|u| u.value == TAMOUNT2).unwrap(); + assert_eq!(txs[&txid2].utxos.len(), 1); + assert_eq!(utxo2.address, taddr2); + assert_eq!(utxo2.txid, txid2); + assert_eq!(utxo2.spent, None); + assert_eq!(utxo2.unconfirmed_spent, None); + } + } + } \ No newline at end of file From c901b4f52f86d16590014128ef4aad5143e6de18 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Mon, 21 Oct 2019 17:25:14 -0700 Subject: [PATCH 2/7] Move tests to tests.rs --- lib/src/lightclient.rs | 2 +- lib/src/lightwallet.rs | 1965 +--------------------------------- lib/src/lightwallet/tests.rs | 1959 +++++++++++++++++++++++++++++++++ 3 files changed, 1961 insertions(+), 1965 deletions(-) create mode 100644 lib/src/lightwallet/tests.rs diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 45dfe32..0373a7e 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -911,7 +911,7 @@ impl LightClient { } } - +#[cfg(test)] pub mod tests { use lazy_static::lazy_static; //use super::LightClient; diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 14a3aed..74f14dd 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -1523,1968 +1523,5 @@ impl LightWallet { } } - - #[cfg(test)] -pub mod tests { - use std::convert::TryInto; - use std::io::{Error}; - use rand::{RngCore, rngs::OsRng}; - - use ff::{Field, PrimeField, PrimeFieldRepr}; - use pairing::bls12_381::Bls12; - use protobuf::{Message, UnknownFields, CachedSize, RepeatedField}; - use zcash_client_backend::{encoding::encode_payment_address, - proto::compact_formats::{ - CompactBlock, CompactOutput, CompactSpend, CompactTx, - } - }; - use zcash_primitives::{ - block::BlockHash, - jubjub::fs::Fs, - note_encryption::{Memo, SaplingNoteEncryption}, - primitives::{Note, PaymentAddress}, - legacy::{Script, TransparentAddress,}, - transaction::{ - TxId, Transaction, TransactionData, - components::{TxOut, TxIn, OutPoint, Amount,}, - components::amount::DEFAULT_FEE, - }, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, - JUBJUB, - }; - - use sha2::{Sha256, Digest}; - - use super::LightWallet; - use super::LightClientConfig; - use secp256k1::{Secp256k1, key::PublicKey, key::SecretKey}; - use crate::SaplingParams; - - fn get_sapling_params() -> Result<(Vec, Vec), Error> { - // Read Sapling Params - let mut sapling_output = vec![]; - sapling_output.extend_from_slice(SaplingParams::get("sapling-output.params").unwrap().as_ref()); - println!("Read output {}", sapling_output.len()); - - let mut sapling_spend = vec![]; - sapling_spend.extend_from_slice(SaplingParams::get("sapling-spend.params").unwrap().as_ref()); - println!("Read output {}", sapling_spend.len()); - - Ok((sapling_spend, sapling_output)) - } - - struct FakeCompactBlock { - block: CompactBlock, - } - - impl FakeCompactBlock { - fn new(height: i32, prev_hash: BlockHash) -> Self { - // Create a fake Note for the account - let mut rng = OsRng; - - let mut cb = CompactBlock::new(); - - cb.set_height(height as u64); - cb.hash.resize(32, 0); - rng.fill_bytes(&mut cb.hash); - - cb.prevHash.extend_from_slice(&prev_hash.0); - - FakeCompactBlock { block: cb } - } - - fn as_bytes(&self) -> Vec { - self.block.write_to_bytes().unwrap() - } - - fn hash(&self) -> BlockHash { - BlockHash(self.block.hash[..].try_into().unwrap()) - } - - fn tx_to_compact_tx(tx: &Transaction, index: u64) -> CompactTx { - let spends = tx.shielded_spends.iter().map(|s| { - let mut c_spend = CompactSpend::default(); - c_spend.set_nf(s.nullifier.to_vec()); - - c_spend - }).collect::>(); - - let outputs = tx.shielded_outputs.iter().map(|o| { - let mut c_out = CompactOutput::default(); - - let mut cmu_bytes = vec![]; - o.cmu.into_repr().write_le(&mut cmu_bytes).unwrap(); - - let mut epk_bytes = vec![]; - o.ephemeral_key.write(&mut epk_bytes).unwrap(); - - c_out.set_cmu(cmu_bytes); - c_out.set_epk(epk_bytes); - c_out.set_ciphertext(o.enc_ciphertext[0..52].to_vec()); - - c_out - }).collect::>(); - - CompactTx { - index, - hash: tx.txid().0.to_vec(), - fee: 0, // TODO: Get Fee - spends: RepeatedField::from_vec(spends), - outputs: RepeatedField::from_vec(outputs), - unknown_fields: UnknownFields::default(), - cached_size: CachedSize::default(), - } - } - - // Convert the transaction into a CompactTx and add it to this block - fn add_tx(&mut self, tx: &Transaction) { - let ctx = FakeCompactBlock::tx_to_compact_tx(&tx, self.block.vtx.len() as u64); - self.block.vtx.push(ctx); - } - - // Add a new tx into the block, paying the given address the amount. - // Returns the nullifier of the new note. - fn add_tx_paying(&mut self, extfvk: ExtendedFullViewingKey, value: u64) - -> (Vec, TxId) { - let to = extfvk.default_address().unwrap().1; - let value = Amount::from_u64(value).unwrap(); - - // Create a fake Note for the account - let mut rng = OsRng; - let note = Note { - g_d: to.diversifier.g_d::(&JUBJUB).unwrap(), - pk_d: to.pk_d.clone(), - value: value.into(), - r: Fs::random(&mut rng), - }; - let encryptor = SaplingNoteEncryption::new( - extfvk.fvk.ovk, - note.clone(), - to.clone(), - Memo::default(), - &mut rng, - ); - let mut cmu = vec![]; - note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap(); - let mut epk = vec![]; - encryptor.epk().write(&mut epk).unwrap(); - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - - // Create a fake CompactBlock containing the note - let mut cout = CompactOutput::new(); - cout.set_cmu(cmu); - cout.set_epk(epk); - cout.set_ciphertext(enc_ciphertext[..52].to_vec()); - let mut ctx = CompactTx::new(); - let mut txid = vec![0; 32]; - rng.fill_bytes(&mut txid); - ctx.set_hash(txid.clone()); - ctx.outputs.push(cout); - - self.block.vtx.push(ctx); - (note.nf(&extfvk.fvk.vk, 0, &JUBJUB), TxId(txid[..].try_into().unwrap())) - } - - fn add_tx_spending(&mut self, - (nf, in_value): (Vec, u64), - extfvk: ExtendedFullViewingKey, - to: PaymentAddress, - value: u64) -> TxId { - let mut rng = OsRng; - - let in_value = Amount::from_u64(in_value).unwrap(); - let value = Amount::from_u64(value).unwrap(); - - // Create a fake CompactBlock containing the note - let mut cspend = CompactSpend::new(); - cspend.set_nf(nf); - let mut ctx = CompactTx::new(); - let mut txid = vec![0; 32]; - rng.fill_bytes(&mut txid); - ctx.set_hash(txid.clone()); - ctx.spends.push(cspend); - - // Create a fake Note for the payment - ctx.outputs.push({ - let note = Note { - g_d: to.diversifier.g_d::(&JUBJUB).unwrap(), - pk_d: to.pk_d.clone(), - value: value.into(), - r: Fs::random(&mut rng), - }; - let encryptor = SaplingNoteEncryption::new( - extfvk.fvk.ovk, - note.clone(), - to, - Memo::default(), - &mut rng, - ); - let mut cmu = vec![]; - note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap(); - let mut epk = vec![]; - encryptor.epk().write(&mut epk).unwrap(); - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - - let mut cout = CompactOutput::new(); - cout.set_cmu(cmu); - cout.set_epk(epk); - cout.set_ciphertext(enc_ciphertext[..52].to_vec()); - cout - }); - - // Create a fake Note for the change - ctx.outputs.push({ - let change_addr = extfvk.default_address().unwrap().1; - let note = Note { - g_d: change_addr.diversifier.g_d::(&JUBJUB).unwrap(), - pk_d: change_addr.pk_d.clone(), - value: (in_value - value).into(), - r: Fs::random(&mut rng), - }; - let encryptor = SaplingNoteEncryption::new( - extfvk.fvk.ovk, - note.clone(), - change_addr, - Memo::default(), - &mut rng, - ); - let mut cmu = vec![]; - note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap(); - let mut epk = vec![]; - encryptor.epk().write(&mut epk).unwrap(); - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - - let mut cout = CompactOutput::new(); - cout.set_cmu(cmu); - cout.set_epk(epk); - cout.set_ciphertext(enc_ciphertext[..52].to_vec()); - cout - }); - - self.block.vtx.push(ctx); - - TxId(txid[..].try_into().unwrap()) - } - } - - struct FakeTransaction { - tx: Transaction, - } - - impl FakeTransaction { - // New FakeTransaction with random txid - fn new(rng: &mut R) -> Self { - let mut txid = [0u8; 32]; - rng.fill_bytes(&mut txid); - FakeTransaction::new_with_txid(TxId(txid)) - } - - fn new_with_txid(txid: TxId) -> Self { - FakeTransaction { - tx: Transaction { - txid, - data: TransactionData::new() - } - } - } - - fn get_tx(&self) -> &Transaction { - &self.tx - } - - fn add_t_output(&mut self, pk: &PublicKey, value: u64) { - let mut hash160 = ripemd160::Ripemd160::new(); - hash160.input(Sha256::digest(&pk.serialize()[..].to_vec())); - - let taddr_bytes = hash160.result(); - - self.tx.data.vout.push(TxOut { - value: Amount::from_u64(value).unwrap(), - script_pubkey: TransparentAddress::PublicKey(taddr_bytes.try_into().unwrap()).script(), - }); - } - - fn add_t_input(&mut self, txid: TxId, n: u32) { - self.tx.data.vin.push(TxIn { - prevout: OutPoint{ - hash: txid.0, - n - }, - script_sig: Script{0: vec![]}, - sequence: 0, - }); - } - } - - #[test] - fn test_z_balances() { - let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); - - const AMOUNT1:u64 = 5; - // 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)); - - let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); - cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); - - // Make sure that the intial state is empty - assert_eq!(wallet.txs.read().unwrap().len(), 0); - assert_eq!(wallet.blocks.read().unwrap().len(), 0); - assert_eq!(wallet.zbalance(None), 0); - assert_eq!(wallet.zbalance(address.clone()), 0); - - wallet.scan_block(&cb1.as_bytes()).unwrap(); - - assert_eq!(wallet.txs.read().unwrap().len(), 1); - assert_eq!(wallet.blocks.read().unwrap().len(), 1); - assert_eq!(wallet.zbalance(None), AMOUNT1); - assert_eq!(wallet.zbalance(address.clone()), AMOUNT1); - - const AMOUNT2:u64 = 10; - - // Add a second block - let mut cb2 = FakeCompactBlock::new(1, cb1.hash()); - cb2.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT2); - - wallet.scan_block(&cb2.as_bytes()).unwrap(); - - assert_eq!(wallet.txs.read().unwrap().len(), 2); - assert_eq!(wallet.blocks.read().unwrap().len(), 2); - assert_eq!(wallet.zbalance(None), AMOUNT1 + AMOUNT2); - assert_eq!(wallet.zbalance(address.clone()), AMOUNT1 + AMOUNT2); - } - - #[test] - fn test_z_change_balances() { - let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); - - // First, add an incoming transaction - const AMOUNT1:u64 = 5; - - let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); - let (nf1, txid1) = cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); - - wallet.scan_block(&cb1.as_bytes()).unwrap(); - - assert_eq!(wallet.txs.read().unwrap().len(), 1); - assert_eq!(wallet.blocks.read().unwrap().len(), 1); - assert_eq!(wallet.zbalance(None), AMOUNT1); - - const AMOUNT2:u64 = 2; - - // Add a second block, spending the first note - 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); - wallet.scan_block(&cb2.as_bytes()).unwrap(); - - // Now, the original note should be spent and there should be a change - assert_eq!(wallet.zbalance(None), AMOUNT1 - AMOUNT2); - - let txs = wallet.txs.read().unwrap(); - - // Old note was spent - assert_eq!(txs[&txid1].txid, txid1); - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].spent.unwrap(), txid2); - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); - assert_eq!(txs[&txid1].notes[0].is_change, false); - - // new note is not spent - assert_eq!(txs[&txid2].txid, txid2); - assert_eq!(txs[&txid2].notes.len(), 1); - assert_eq!(txs[&txid2].notes[0].spent, None); - assert_eq!(txs[&txid2].notes[0].note.value, AMOUNT1 - AMOUNT2); - assert_eq!(txs[&txid2].notes[0].is_change, true); - assert_eq!(txs[&txid2].total_shielded_value_spent, AMOUNT1); - } - - #[test] - fn test_t_receive_spend() { - let mut rng = OsRng; - let secp = Secp256k1::new(); - - let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); - - let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); - let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); - - const AMOUNT1: u64 = 20; - - let mut tx = FakeTransaction::new(&mut rng); - tx.add_t_output(&pk, AMOUNT1); - let txid1 = tx.get_tx().txid(); - - wallet.scan_full_tx(&tx.get_tx(), 100, 0); // Pretend it is at height 100 - - { - let txs = wallet.txs.read().unwrap(); - - // Now make sure the t addr was recieved - assert_eq!(txs.len(), 1); - assert_eq!(txs[&txid1].utxos.len(), 1); - assert_eq!(txs[&txid1].utxos[0].address, taddr); - assert_eq!(txs[&txid1].utxos[0].txid, txid1); - assert_eq!(txs[&txid1].utxos[0].output_index, 0); - assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1); - assert_eq!(txs[&txid1].utxos[0].height, 100); - assert_eq!(txs[&txid1].utxos[0].spent, None); - assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None); - - assert_eq!(wallet.tbalance(None), AMOUNT1); - assert_eq!(wallet.tbalance(Some(taddr)), AMOUNT1); - } - - // Create a new Tx, spending this taddr - let mut tx = FakeTransaction::new(&mut rng); - tx.add_t_input(txid1, 0); - let txid2 = tx.get_tx().txid(); - - wallet.scan_full_tx(&tx.get_tx(), 101, 0); // Pretent it is at height 101 - - { - // Make sure the txid was spent - let txs = wallet.txs.read().unwrap(); - - // Old utxo, that should be spent now - assert_eq!(txs.len(), 2); - assert_eq!(txs[&txid1].utxos.len(), 1); - assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1); - assert_eq!(txs[&txid1].utxos[0].spent, Some(txid2)); - assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None); - - assert_eq!(txs[&txid2].block, 101); // The second TxId is at block 101 - assert_eq!(txs[&txid2].utxos.len(), 0); // The second TxId has no UTXOs - assert_eq!(txs[&txid2].total_transparent_value_spent, AMOUNT1); - - // Make sure there is no t-ZEC left - assert_eq!(wallet.tbalance(None), 0); - } - } - - - #[test] - /// This test spends and receives t addresses among non-wallet t addresses to make sure that - /// we're detecting and spending only our t addrs. - fn test_t_receive_spend_among_tadds() { - let mut rng = OsRng; - let secp = Secp256k1::new(); - - let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); - - let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); - let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); - - let non_wallet_sk = &SecretKey::from_slice(&[1u8; 32]).unwrap(); - let non_wallet_pk = PublicKey::from_secret_key(&secp, &non_wallet_sk); - - const AMOUNT1: u64 = 30; - - let mut tx = FakeTransaction::new(&mut rng); - // Add a non-wallet output - tx.add_t_output(&non_wallet_pk, 20); - tx.add_t_output(&pk, AMOUNT1); // Our wallet t output - tx.add_t_output(&non_wallet_pk, 25); - let txid1 = tx.get_tx().txid(); - - wallet.scan_full_tx(&tx.get_tx(), 100, 0); // Pretend it is at height 100 - - { - let txs = wallet.txs.read().unwrap(); - - // Now make sure the t addr was received - assert_eq!(txs.len(), 1); - assert_eq!(txs[&txid1].utxos.len(), 1); - assert_eq!(txs[&txid1].utxos[0].address, taddr); - assert_eq!(txs[&txid1].utxos[0].txid, txid1); - assert_eq!(txs[&txid1].utxos[0].output_index, 1); - assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1); - assert_eq!(txs[&txid1].utxos[0].height, 100); - assert_eq!(txs[&txid1].utxos[0].spent, None); - assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None); - - assert_eq!(wallet.tbalance(None), AMOUNT1); - assert_eq!(wallet.tbalance(Some(taddr)), AMOUNT1); - } - - // Create a new Tx, spending this taddr - let mut tx = FakeTransaction::new(&mut rng); - tx.add_t_input(txid1, 1); // Ours was at position 1 in the input tx - let txid2 = tx.get_tx().txid(); - - wallet.scan_full_tx(&tx.get_tx(), 101, 0); // Pretent it is at height 101 - - { - // Make sure the txid was spent - let txs = wallet.txs.read().unwrap(); - - // Old utxo, that should be spent now - assert_eq!(txs.len(), 2); - assert_eq!(txs[&txid1].utxos.len(), 1); - assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1); - assert_eq!(txs[&txid1].utxos[0].spent, Some(txid2)); - assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None); - - assert_eq!(txs[&txid2].block, 101); // The second TxId is at block 101 - assert_eq!(txs[&txid2].utxos.len(), 0); // The second TxId has no UTXOs - assert_eq!(txs[&txid2].total_transparent_value_spent, AMOUNT1); - - // Make sure there is no t-ZEC left - assert_eq!(wallet.tbalance(None), 0); - } - } - - #[test] - fn test_serialization() { - let secp = Secp256k1::new(); - let config = get_test_config(); - - let wallet = LightWallet::new(None, &config, 0).unwrap(); - - // First, add an incoming transaction - const AMOUNT1:u64 = 5; - - let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); - let (nf1, txid1) = cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); - - wallet.scan_block(&cb1.as_bytes()).unwrap(); - - assert_eq!(wallet.txs.read().unwrap().len(), 1); - assert_eq!(wallet.blocks.read().unwrap().len(), 1); - assert_eq!(wallet.zbalance(None), AMOUNT1); - - // Add a t input at the Tx - let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); - let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); - - const TAMOUNT1: u64 = 20; - - let mut tx = FakeTransaction::new_with_txid(txid1); - tx.add_t_output(&pk, TAMOUNT1); - wallet.scan_full_tx(&tx.get_tx(), 0, 0); // Height 0 - - const AMOUNT2:u64 = 2; - - // Add a second block, spending the first note - 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); - wallet.scan_block(&cb2.as_bytes()).unwrap(); - - let mut tx = FakeTransaction::new_with_txid(txid2); - tx.add_t_input(txid1, 0); - wallet.scan_full_tx(&tx.get_tx(), 1, 0); // Height 1 - - // Now, the original note should be spent and there should be a change - assert_eq!(wallet.zbalance(None), AMOUNT1 - AMOUNT2 ); // The t addr amount is received + spent, so it cancels out - - // Now, serialize the wallet and read it back again - let mut serialized_data = vec![]; - wallet.write(&mut serialized_data).expect("Serialize wallet"); - let wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap(); - - assert_eq!(wallet2.zbalance(None), AMOUNT1 - AMOUNT2); - - // Test the keys were serialized correctly - { - 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.tkeys.read().unwrap().len(), wallet2.tkeys.read().unwrap().len()); - assert_eq!(wallet.tkeys.read().unwrap()[0], wallet2.tkeys.read().unwrap()[0]); - } - - // Test blocks were serialized properly - { - let blks = wallet2.blocks.read().unwrap(); - - assert_eq!(blks.len(), 2); - assert_eq!(blks[0].height, 0); - assert_eq!(blks[1].height, 1); - } - - // Test txns were serialized properly. - { - let txs = wallet2.txs.read().unwrap(); - - // Old note was spent - assert_eq!(txs[&txid1].txid, txid1); - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].spent.unwrap(), txid2); - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); - assert_eq!(txs[&txid1].notes[0].is_change, false); - - // Old UTXO was spent - assert_eq!(txs[&txid1].utxos.len(), 1); - assert_eq!(txs[&txid1].utxos[0].address, taddr); - assert_eq!(txs[&txid1].utxos[0].txid, txid1); - assert_eq!(txs[&txid1].utxos[0].output_index, 0); - assert_eq!(txs[&txid1].utxos[0].value, TAMOUNT1); - assert_eq!(txs[&txid1].utxos[0].height, 0); - assert_eq!(txs[&txid1].utxos[0].spent, Some(txid2)); - assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None); - - // new note is not spent - assert_eq!(txs[&txid2].txid, txid2); - assert_eq!(txs[&txid2].notes.len(), 1); - assert_eq!(txs[&txid2].notes[0].spent, None); - assert_eq!(txs[&txid2].notes[0].note.value, AMOUNT1 - AMOUNT2); - assert_eq!(txs[&txid2].notes[0].is_change, true); - assert_eq!(txs[&txid2].total_shielded_value_spent, AMOUNT1); - - // The UTXO was spent in txid2 - assert_eq!(txs[&txid2].utxos.len(), 0); // The second TxId has no UTXOs - assert_eq!(txs[&txid2].total_transparent_value_spent, TAMOUNT1); - } - } - - #[test] - fn test_multi_serialization() { - let config = get_test_config(); - - let wallet = LightWallet::new(None, &config, 0).unwrap(); - - let taddr1 = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); - let taddr2 = wallet.add_taddr(); - - let (zaddr1, zpk1) = &wallet.get_z_private_keys()[0]; - let zaddr2 = wallet.add_zaddr(); - - let mut serialized_data = vec![]; - wallet.write(&mut serialized_data).expect("Serialize wallet"); - let wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap(); - - assert_eq!(wallet2.tkeys.read().unwrap().len(), 2); - assert_eq!(wallet2.extsks.read().unwrap().len(), 2); - assert_eq!(wallet2.extfvks.read().unwrap().len(), 2); - assert_eq!(wallet2.zaddress.read().unwrap().len(), 2); - - assert_eq!(taddr1, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0])); - assert_eq!(taddr2, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[1])); - - let (w2_zaddr1, w2_zpk1) = &wallet.get_z_private_keys()[0]; - let (w2_zaddr2, _) = &wallet.get_z_private_keys()[1]; - assert_eq!(zaddr1, w2_zaddr1); - assert_eq!(zpk1, w2_zpk1); - assert_eq!(zaddr2, *w2_zaddr2); - - } - - fn get_test_config() -> LightClientConfig { - LightClientConfig { - server: "0.0.0.0:0".parse().unwrap(), - chain_name: "test".to_string(), - sapling_activation_height: 0, - consensus_branch_id: "000000".to_string(), - anchor_offset: 0, - no_cert_verification: false, - } - } - - // Get a test wallet already setup with a single note - fn get_test_wallet(amount: u64) -> (LightWallet, TxId, BlockHash) { - let config = get_test_config(); - - 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); - wallet.scan_block(&cb1.as_bytes()).unwrap(); - - // We have one note - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].note.value, amount); - assert_eq!(txs[&txid1].notes[0].spent, None); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); - } - - assert_eq!(wallet.verified_zbalance(None), amount); - - // Create a new block so that the note is now verified to be spent - let cb2 = FakeCompactBlock::new(1, cb1.hash()); - wallet.scan_block(&cb2.as_bytes()).unwrap(); - - (wallet, txid1, cb2.hash()) - } - - #[test] - fn test_z_spend() { - const AMOUNT1: u64 = 50000; - let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); - - 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 outgoing_memo = "Outgoing Memo".to_string(); - let fee: u64 = DEFAULT_FEE.try_into().unwrap(); - - let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) = get_sapling_params().unwrap(); - - // Create a tx and send to address - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); - - let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); - let sent_txid = sent_tx.txid(); - - // Now, the note should be unconfirmed spent - { - let txs = wallet.txs.read().unwrap(); - - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); - assert_eq!(txs[&txid1].notes[0].is_change, false); - assert_eq!(txs[&txid1].notes[0].spent, None); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, Some(sent_txid)); - } - - let mut cb3 = FakeCompactBlock::new(2, block_hash); - cb3.add_tx(&sent_tx); - wallet.scan_block(&cb3.as_bytes()).unwrap(); - - // Now this new Spent tx should be in, so the note should be marked confirmed spent - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); - assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); - - // The sent tx should generate change - assert_eq!(txs[&sent_txid].notes.len(), 1); - assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee); - assert_eq!(txs[&sent_txid].notes[0].is_change, true); - assert_eq!(txs[&sent_txid].notes[0].spent, None); - assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); - } - - // Now, full scan the Tx, which should populate the Outgoing Meta data - wallet.scan_full_tx(&sent_tx, 2, 0); - - // Check Outgoing Metadata - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); - - assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); - - assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, ext_address); - assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT); - assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); - } - } - - #[test] - fn test_multi_z() { - const AMOUNT1: u64 = 50000; - let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); - - let zaddr2 = wallet.add_zaddr(); - - const AMOUNT_SENT: u64 = 20; - - let outgoing_memo = "Outgoing Memo".to_string(); - let fee: u64 = DEFAULT_FEE.try_into().unwrap(); - - let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) =get_sapling_params().unwrap(); - - // Create a tx and send to address - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - 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); - - // Because the builder will randomize notes outputted, we need to find - // which note number is the change and which is the output note (Because this tx - // had both outputs in the same Tx) - let (change_note_number, ext_note_number) = { - let txs = wallet.txs.read().unwrap(); - if txs[&sent_txid].notes[0].is_change { (0,1) } else { (1,0) } - }; - - // Now this new Spent tx should be in, so the note should be marked confirmed spent - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); - assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); - - // The sent tx should generate change + the new incoming note - assert_eq!(txs[&sent_txid].notes.len(), 2); - - assert_eq!(txs[&sent_txid].notes[change_note_number].note.value, AMOUNT1 - AMOUNT_SENT - fee); - assert_eq!(txs[&sent_txid].notes[change_note_number].account, 0); - assert_eq!(txs[&sent_txid].notes[change_note_number].is_change, true); - assert_eq!(txs[&sent_txid].notes[change_note_number].spent, None); - assert_eq!(txs[&sent_txid].notes[change_note_number].unconfirmed_spent, None); - assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[change_note_number].memo), None); - - assert_eq!(txs[&sent_txid].notes[ext_note_number].note.value, AMOUNT_SENT); - assert_eq!(txs[&sent_txid].notes[ext_note_number].account, 1); - assert_eq!(txs[&sent_txid].notes[ext_note_number].is_change, false); - assert_eq!(txs[&sent_txid].notes[ext_note_number].spent, None); - assert_eq!(txs[&sent_txid].notes[ext_note_number].unconfirmed_spent, None); - assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[ext_note_number].memo), Some(outgoing_memo)); - - assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); - - // No Outgoing meta data, since this is a wallet -> wallet tx - assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 0); - } - - // Now spend the money, which should pick notes from both addresses - let amount_all:u64 = (AMOUNT1 - AMOUNT_SENT - fee) + (AMOUNT_SENT) - fee; - let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); - - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&taddr, amount_all, None)]).unwrap(); - let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); - let sent_ext_txid = sent_tx.txid(); - - let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); - cb4.add_tx(&sent_tx); - wallet.scan_block(&cb4.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 3, 0); - - { - // Both notes should be spent now. - let txs = wallet.txs.read().unwrap(); - - assert_eq!(txs[&sent_txid].notes[change_note_number].is_change, true); - assert_eq!(txs[&sent_txid].notes[change_note_number].spent, Some(sent_ext_txid)); - assert_eq!(txs[&sent_txid].notes[change_note_number].unconfirmed_spent, None); - - assert_eq!(txs[&sent_txid].notes[ext_note_number].is_change, false); - assert_eq!(txs[&sent_txid].notes[ext_note_number].spent, Some(sent_ext_txid)); - assert_eq!(txs[&sent_txid].notes[ext_note_number].unconfirmed_spent, None); - - // Check outgoing metadata for the external sent tx - assert_eq!(txs[&sent_ext_txid].notes.len(), 0); // No change was generated - assert_eq!(txs[&sent_ext_txid].outgoing_metadata.len(), 1); - assert_eq!(txs[&sent_ext_txid].outgoing_metadata[0].address, taddr); - assert_eq!(txs[&sent_ext_txid].outgoing_metadata[0].value, amount_all); - } - } - - #[test] - fn test_z_spend_to_taddr() { - const AMOUNT1: u64 = 50000; - let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); - - let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) = get_sapling_params().unwrap(); - - let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); - const AMOUNT_SENT: u64 = 30; - 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 sent_tx = Transaction::read(&raw_tx[..]).unwrap(); - let sent_txid = sent_tx.txid(); - - // Now, the note should be unconfirmed spent - { - let txs = wallet.txs.read().unwrap(); - - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); - assert_eq!(txs[&txid1].notes[0].is_change, false); - assert_eq!(txs[&txid1].notes[0].spent, None); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, Some(sent_txid)); - } - - let mut cb3 = FakeCompactBlock::new(2, block_hash); - cb3.add_tx(&sent_tx); - wallet.scan_block(&cb3.as_bytes()).unwrap(); - - - // Now this new Spent tx should be in, so the note should be marked confirmed spent - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); - assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); - - // The sent tx should generate change - assert_eq!(txs[&sent_txid].notes.len(), 1); - assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee); - assert_eq!(txs[&sent_txid].notes[0].is_change, true); - assert_eq!(txs[&sent_txid].notes[0].spent, None); - assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); - } - - // Now, full scan the Tx, which should populate the Outgoing Meta data - wallet.scan_full_tx(&sent_tx, 2, 0); - - // Check Outgoing Metadata for t address - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); - assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, taddr); - assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT); - assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); - } - } - - #[test] - fn test_t_spend_to_z() { - let mut rng = OsRng; - let secp = Secp256k1::new(); - - const AMOUNT_Z: u64 = 50000; - const AMOUNT_T: u64 = 40000; - let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT_Z); - - let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); - let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); - - let mut tx = FakeTransaction::new(&mut rng); - tx.add_t_output(&pk, AMOUNT_T); - let txid_t = tx.get_tx().txid(); - - wallet.scan_full_tx(&tx.get_tx(), 1, 0); // Pretend it is at height 1 - - { - let txs = wallet.txs.read().unwrap(); - - // Now make sure the t addr was recieved - assert_eq!(txs[&txid_t].utxos.len(), 1); - assert_eq!(txs[&txid_t].utxos[0].address, taddr); - assert_eq!(txs[&txid_t].utxos[0].spent, None); - assert_eq!(txs[&txid_t].utxos[0].unconfirmed_spent, None); - - assert_eq!(wallet.tbalance(None), AMOUNT_T); - } - - - 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 outgoing_memo = "Outgoing Memo".to_string(); - let fee: u64 = DEFAULT_FEE.try_into().unwrap(); - - let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) =get_sapling_params().unwrap(); - - // Create a tx and send to address. 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 sent_tx = Transaction::read(&raw_tx[..]).unwrap(); - let sent_txid = sent_tx.txid(); - - // Verify the sent_tx for sanity - { - // The tx has 1 note spent, 1 utxo spent, and (1 note out, 1 note change) - assert_eq!(sent_tx.shielded_spends.len(), 1); - assert_eq!(sent_tx.vin.len(), 1); - assert_eq!(sent_tx.shielded_outputs.len(), 2); - } - - // Now, the note and utxo should be unconfirmed spent - { - let txs = wallet.txs.read().unwrap(); - - // UTXO - assert_eq!(txs[&txid_t].utxos.len(), 1); - assert_eq!(txs[&txid_t].utxos[0].address, taddr); - assert_eq!(txs[&txid_t].utxos[0].spent, None); - assert_eq!(txs[&txid_t].utxos[0].unconfirmed_spent, Some(sent_txid)); - - // Note - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT_Z); - assert_eq!(txs[&txid1].notes[0].spent, None); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, Some(sent_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); - - // 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, AMOUNT_Z); - assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); - - // The UTXO should also be spent - assert_eq!(txs[&txid_t].utxos[0].address, taddr); - assert_eq!(txs[&txid_t].utxos[0].spent, Some(sent_txid)); - assert_eq!(txs[&txid_t].utxos[0].unconfirmed_spent, None); - - // The sent tx should generate change - assert_eq!(txs[&sent_txid].notes.len(), 1); - assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT_Z + AMOUNT_T - AMOUNT_SENT - fee); - assert_eq!(txs[&sent_txid].notes[0].is_change, true); - assert_eq!(txs[&sent_txid].notes[0].spent, None); - assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); - } - } - - #[test] - fn test_z_incoming_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 memo = "Incoming Memo".to_string(); - let fee: u64 = DEFAULT_FEE.try_into().unwrap(); - - let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) = get_sapling_params().unwrap(); - - // Create a tx and send to address - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - 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(memo)); - } - } - - #[test] - fn test_z_to_t_withinwallet() { - const AMOUNT: u64 = 500000; - const AMOUNT_SENT: u64 = 20000; - let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT); - - let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); - - 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![(&taddr, AMOUNT_SENT, None)]).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(); - - // We have the original note - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT); - - // We have the spent tx - assert_eq!(txs[&sent_txid].notes.len(), 1); - assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT - AMOUNT_SENT - fee); - assert_eq!(txs[&sent_txid].notes[0].is_change, true); - assert_eq!(txs[&sent_txid].notes[0].spent, None); - assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); - - // Since we sent the Tx to ourself, there should be no outgoing - // metadata - assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT); - assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 0); - - - // We have the taddr utxo in the same Tx - assert_eq!(txs[&sent_txid].utxos.len(), 1); - assert_eq!(txs[&sent_txid].utxos[0].address, taddr); - assert_eq!(txs[&sent_txid].utxos[0].value, AMOUNT_SENT); - assert_eq!(txs[&sent_txid].utxos[0].spent, None); - assert_eq!(txs[&sent_txid].utxos[0].unconfirmed_spent, None); - - } - } - - #[test] - fn test_multi_t() { - const AMOUNT: u64 = 5000000; - const AMOUNT_SENT1: u64 = 20000; - const AMOUNT_SENT2: u64 = 10000; - - let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT); - - // Add a new taddr - let taddr2 = wallet.add_taddr(); - - let fee: u64 = DEFAULT_FEE.try_into().unwrap(); - - let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) = get_sapling_params().unwrap(); - - // Create a Tx and send to the second t address - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&taddr2, AMOUNT_SENT1, None)]).unwrap(); - let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); - let sent_txid1 = sent_tx.txid(); - - // Add it to a block - let mut cb3 = FakeCompactBlock::new(2, block_hash); - cb3.add_tx(&sent_tx); - wallet.scan_block(&cb3.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 2, 0); - - // Check that the send to the second taddr worked - { - let txs = wallet.txs.read().unwrap(); - - // We have the original note - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT); - - // We have the spent tx - assert_eq!(txs[&sent_txid1].notes.len(), 1); - assert_eq!(txs[&sent_txid1].notes[0].note.value, AMOUNT - AMOUNT_SENT1 - fee); - assert_eq!(txs[&sent_txid1].notes[0].is_change, true); - assert_eq!(txs[&sent_txid1].notes[0].spent, None); - assert_eq!(txs[&sent_txid1].notes[0].unconfirmed_spent, None); - - // Since we sent the Tx to ourself, there should be no outgoing - // metadata - assert_eq!(txs[&sent_txid1].total_shielded_value_spent, AMOUNT); - assert_eq!(txs[&sent_txid1].outgoing_metadata.len(), 0); - - - // We have the taddr utxo in the same Tx - assert_eq!(txs[&sent_txid1].utxos.len(), 1); - assert_eq!(txs[&sent_txid1].utxos[0].address, taddr2); - assert_eq!(txs[&sent_txid1].utxos[0].value, AMOUNT_SENT1); - assert_eq!(txs[&sent_txid1].utxos[0].spent, None); - assert_eq!(txs[&sent_txid1].utxos[0].unconfirmed_spent, None); - } - - // Send some money to the 3rd t addr - let taddr3 = wallet.add_taddr(); - - // Create a Tx and send to the second t address - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&taddr3, AMOUNT_SENT2, None)]).unwrap(); - let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); - let sent_txid2 = sent_tx.txid(); - - // Add it to a block - let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); - cb4.add_tx(&sent_tx); - wallet.scan_block(&cb4.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 3, 0); - - // Quickly check we have it - { - let txs = wallet.txs.read().unwrap(); - - // We have the taddr utxo in the same Tx - assert_eq!(txs[&sent_txid2].utxos.len(), 1); - assert_eq!(txs[&sent_txid2].utxos[0].address, taddr3); - assert_eq!(txs[&sent_txid2].utxos[0].value, AMOUNT_SENT2); - - // Old UTXO was spent here - assert_eq!(txs[&sent_txid1].utxos.len(), 1); - assert_eq!(txs[&sent_txid1].utxos[0].value, AMOUNT_SENT1); - assert_eq!(txs[&sent_txid1].utxos[0].address, taddr2); - assert_eq!(txs[&sent_txid1].utxos[0].spent, Some(sent_txid2)); - assert_eq!(txs[&sent_txid1].utxos[0].unconfirmed_spent, None); - } - - // Now, spend to an external z address, which will select all the utxos - let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32])); - let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(), - &fvk.default_address().unwrap().1); - - const AMOUNT_SENT_EXT: u64 = 45; - let outgoing_memo = "Outgoing Memo".to_string(); - - // Create a tx and send to address - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&ext_address, AMOUNT_SENT_EXT, Some(outgoing_memo.clone()))]).unwrap(); - - let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); - let sent_txid3 = sent_tx.txid(); - - let mut cb5 = FakeCompactBlock::new(4, cb4.hash()); - cb5.add_tx(&sent_tx); - wallet.scan_block(&cb5.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 4, 0); - - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&sent_txid3].outgoing_metadata.len(), 1); - - assert_eq!(txs[&sent_txid3].outgoing_metadata[0].address, ext_address); - assert_eq!(txs[&sent_txid3].outgoing_metadata[0].value, AMOUNT_SENT_EXT); - assert_eq!(txs[&sent_txid3].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); - - // Test to see that the UTXOs were spent. - - // UTXO2 - assert_eq!(txs[&sent_txid2].utxos[0].value, AMOUNT_SENT2); - assert_eq!(txs[&sent_txid2].utxos[0].address, taddr3); - assert_eq!(txs[&sent_txid2].utxos[0].spent, Some(sent_txid3)); - assert_eq!(txs[&sent_txid2].utxos[0].unconfirmed_spent, None); - } - - } - - #[test] - fn test_multi_spends() { - const AMOUNT1: u64 = 50000; - let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); - - let zaddr2 = wallet.add_zaddr(); - const ZAMOUNT2:u64 = 30; - let outgoing_memo2 = "Outgoing Memo2".to_string(); - - let zaddr3 = wallet.add_zaddr(); - const ZAMOUNT3:u64 = 40; - let outgoing_memo3 = "Outgoing Memo3".to_string(); - - let taddr2 = wallet.add_taddr(); - const TAMOUNT2:u64 = 50; - let taddr3 = wallet.add_taddr(); - const TAMOUNT3:u64 = 60; - - let fee: u64 = DEFAULT_FEE.try_into().unwrap(); - - let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) = get_sapling_params().unwrap(); - - let tos = vec![ (zaddr2.as_str(), ZAMOUNT2, Some(outgoing_memo2.clone())), - (zaddr3.as_str(), ZAMOUNT3, Some(outgoing_memo3.clone())), - (taddr2.as_str(), TAMOUNT2, None), - (taddr3.as_str(), TAMOUNT3, None) ]; - - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, tos).unwrap(); - let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); - let sent_txid = sent_tx.txid(); - - let mut cb3 = FakeCompactBlock::new(2, block_hash); - cb3.add_tx(&sent_tx); - wallet.scan_block(&cb3.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 2, 0); - - // Make sure all the outputs are there! - { - let txs = wallet.txs.read().unwrap(); - - // The single note was spent - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); - assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); - - // The outputs are all sent to the wallet, so they should - // correspond to notes & utxos. - // 2 notes + 1 change - assert_eq!(txs[&sent_txid].notes.len(), 3); - - // Find the change note - let change_note = txs[&sent_txid].notes.iter().find(|n| n.is_change).unwrap(); - assert_eq!(change_note.note.value, AMOUNT1 - (ZAMOUNT2+ZAMOUNT3+TAMOUNT2+TAMOUNT3+fee)); - assert_eq!(change_note.spent, None); - assert_eq!(change_note.unconfirmed_spent, None); - - // Find zaddr2 - let zaddr2_note = txs[&sent_txid].notes.iter().find(|n| n.note.value == ZAMOUNT2).unwrap(); - assert_eq!(zaddr2_note.account, 2-1); - assert_eq!(zaddr2_note.is_change, false); - assert_eq!(zaddr2_note.spent, None); - assert_eq!(zaddr2_note.unconfirmed_spent, None); - assert_eq!(LightWallet::memo_str(&zaddr2_note.memo), Some(outgoing_memo2)); - - // Find zaddr3 - let zaddr3_note = txs[&sent_txid].notes.iter().find(|n| n.note.value == ZAMOUNT3).unwrap(); - assert_eq!(zaddr3_note.account, 3-1); - assert_eq!(zaddr3_note.is_change, false); - assert_eq!(zaddr3_note.spent, None); - assert_eq!(zaddr3_note.unconfirmed_spent, None); - assert_eq!(LightWallet::memo_str(&zaddr3_note.memo), Some(outgoing_memo3)); - - // Find taddr2 - let utxo2 = txs[&sent_txid].utxos.iter().find(|u| u.value == TAMOUNT2).unwrap(); - assert_eq!(utxo2.address, taddr2); - assert_eq!(utxo2.txid, sent_txid); - assert_eq!(utxo2.spent, None); - assert_eq!(utxo2.unconfirmed_spent, None); - - // Find taddr3 - let utxo3 = txs[&sent_txid].utxos.iter().find(|u| u.value == TAMOUNT3).unwrap(); - assert_eq!(utxo3.address, taddr3); - assert_eq!(utxo3.txid, sent_txid); - assert_eq!(utxo3.spent, None); - assert_eq!(utxo3.unconfirmed_spent, None); - } - - // Now send an outgoing tx to one ext taddr and one ext zaddr - let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32])); - let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(), - &fvk.default_address().unwrap().1); - let ext_memo = "External memo".to_string(); - let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); - - const EXT_ZADDR_AMOUNT: u64 = 3000; - let ext_taddr_amount = AMOUNT1 - fee - EXT_ZADDR_AMOUNT - fee; // Spend everything - println!("taddr amount {}", ext_taddr_amount); - - let tos = vec![ (ext_address.as_str(), EXT_ZADDR_AMOUNT, Some(ext_memo.clone())), - (ext_taddr.as_str(), ext_taddr_amount, None)]; - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, tos).unwrap(); - let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); - let sent_txid2 = sent_tx.txid(); - - let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); - cb4.add_tx(&sent_tx); - wallet.scan_block(&cb4.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 3, 0); - - // Make sure all the outputs are there! - { - let txs = wallet.txs.read().unwrap(); - - // All notes were spent - assert_eq!(txs[&sent_txid].notes[0].spent, Some(sent_txid2)); - assert_eq!(txs[&sent_txid].notes[1].spent, Some(sent_txid2)); - assert_eq!(txs[&sent_txid].notes[2].spent, Some(sent_txid2)); - - // All utxos were spent - assert_eq!(txs[&sent_txid].utxos[0].spent, Some(sent_txid2)); - assert_eq!(txs[&sent_txid].utxos[1].spent, Some(sent_txid2)); - - // The new tx has no change - assert_eq!(txs[&sent_txid2].notes.len(), 0); - assert_eq!(txs[&sent_txid2].utxos.len(), 0); - - // Test the outgoing metadata - // Find the znote - let zoutgoing = txs[&sent_txid2].outgoing_metadata.iter().find(|o| o.address == ext_address).unwrap(); - assert_eq!(zoutgoing.value, EXT_ZADDR_AMOUNT); - assert_eq!(LightWallet::memo_str(&Some(zoutgoing.memo.clone())), Some(ext_memo)); - - // Find the taddr - let toutgoing = txs[&sent_txid2].outgoing_metadata.iter().find(|o| o.address == ext_taddr).unwrap(); - assert_eq!(toutgoing.value, ext_taddr_amount); - assert_eq!(LightWallet::memo_str(&Some(toutgoing.memo.clone())), None); - } - } - - #[test] - fn test_bad_send() { - // Test all the ways in which a send should fail - const AMOUNT1: u64 = 50000; - let _fee: u64 = DEFAULT_FEE.try_into().unwrap(); - - let (wallet, _txid1, _block_hash) = get_test_wallet(AMOUNT1); - - let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) = get_sapling_params().unwrap(); - let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); - - // Bad address - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&"badaddress", 10, None)]); - assert!(raw_tx.err().unwrap().contains("Invalid recipient address")); - - // Insufficient funds - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&ext_taddr, AMOUNT1 + 10, None)]); - assert!(raw_tx.err().unwrap().contains("Insufficient verified funds")); - } - - #[test] - #[should_panic] - fn test_bad_params() { - let (wallet, _, _) = get_test_wallet(100000); - let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); - - let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - // Bad params - let _ = wallet.send_to_address(branch_id, &[], &[], - vec![(&ext_taddr, 10, None)]); - } - - /// Test helper to add blocks - fn add_blocks(wallet: &LightWallet, start: i32, num: i32, mut prev_hash: BlockHash) -> Result{ - // Add it to a block - let mut new_blk = FakeCompactBlock::new(start, prev_hash); - for i in 0..num { - new_blk = FakeCompactBlock::new(start+i, prev_hash); - prev_hash = new_blk.hash(); - match wallet.scan_block(&new_blk.as_bytes()) { - Ok(_) => {}, // continue - Err(e) => return Err(e) - }; - } - - Ok(new_blk.hash()) - } - - #[test] - fn test_block_limit() { - const AMOUNT: u64 = 500000; - let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT); - - let prev_hash = add_blocks(&wallet, 2, 1, block_hash).unwrap(); - assert_eq!(wallet.blocks.read().unwrap().len(), 3); - - let prev_hash = add_blocks(&wallet, 3, 47, prev_hash).unwrap(); - assert_eq!(wallet.blocks.read().unwrap().len(), 50); - - let prev_hash = add_blocks(&wallet, 50, 51, prev_hash).unwrap(); - assert_eq!(wallet.blocks.read().unwrap().len(), 101); - - // Subsequent blocks should start to trim - let prev_hash = add_blocks(&wallet, 101, 1, prev_hash).unwrap(); - assert_eq!(wallet.blocks.read().unwrap().len(), 101); - - // Add lots - let _ = add_blocks(&wallet, 102, 10, prev_hash).unwrap(); - assert_eq!(wallet.blocks.read().unwrap().len(), 101); - - // Now clear the blocks - wallet.clear_blocks(); - assert_eq!(wallet.blocks.read().unwrap().len(), 0); - - let prev_hash = add_blocks(&wallet, 0, 1, BlockHash([0;32])).unwrap(); - assert_eq!(wallet.blocks.read().unwrap().len(), 1); - - let _ = add_blocks(&wallet, 1, 10, prev_hash).unwrap(); - assert_eq!(wallet.blocks.read().unwrap().len(), 11); - } - - #[test] - fn test_rollback() { - const AMOUNT: u64 = 500000; - - let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT); - - add_blocks(&wallet, 2, 5, block_hash).unwrap(); - - // Make sure the note exists with the witnesses - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&txid1].notes[0].witnesses.len(), 7); - } - - // Invalidate 2 blocks - assert_eq!(wallet.last_scanned_height(), 6); - assert_eq!(wallet.invalidate_block(5), 2); - - // THe witnesses should be rolledback - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&txid1].notes[0].witnesses.len(), 5); - } - - let blk3_hash; - let blk4_hash; - { - let blks = wallet.blocks.read().unwrap(); - blk3_hash = blks[3].hash.clone(); - blk4_hash = blks[4].hash.clone(); - } - - // This should result in an exception, because the "prevhash" is wrong - assert!(add_blocks(&wallet, 5, 2, blk3_hash).is_err(), - "Shouldn't be able to add because of invalid prev hash"); - - // Add with the proper prev hash - add_blocks(&wallet, 5, 2, blk4_hash).unwrap(); - - let blk6_hash; - { - let blks = wallet.blocks.read().unwrap(); - blk6_hash = blks[6].hash.clone(); - } - - // Now do a Tx - let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).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 - 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 sent_tx = Transaction::read(&raw_tx[..]).unwrap(); - let sent_txid = sent_tx.txid(); - let mut cb3 = FakeCompactBlock::new(7, blk6_hash); - cb3.add_tx(&sent_tx); - wallet.scan_block(&cb3.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx, 7, 0); - - // Make sure the Tx is in. - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT); - assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); - - // The sent tx should generate change - assert_eq!(txs[&sent_txid].notes.len(), 1); - assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT - AMOUNT_SENT - fee); - assert_eq!(txs[&sent_txid].notes[0].is_change, true); - assert_eq!(txs[&sent_txid].notes[0].spent, None); - assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); - assert_eq!(txs[&sent_txid].notes[0].witnesses.len(), 1); - } - - // Invalidate 3 blocks - assert_eq!(wallet.last_scanned_height(), 7); - assert_eq!(wallet.invalidate_block(5), 3); - assert_eq!(wallet.last_scanned_height(), 4); - - // Make sure the orig Tx is there, but new Tx has disappeared - { - let txs = wallet.txs.read().unwrap(); - - // Orig Tx is still there, since this is in block 0 - // But now the spent tx is gone - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT); - assert_eq!(txs[&txid1].notes[0].spent, None); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); - - // The sent tx is missing - assert!(txs.get(&sent_txid).is_none()); - } - } - - #[test] - fn test_t_derivation() { - let lc = 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: 1, - no_cert_verification: false, - }; - - let seed_phrase = Some("chimney better bulb horror rebuild whisper improve intact letter giraffe brave rib appear bulk aim burst snap salt hill sad merge tennis phrase raise".to_string()); - - let wallet = LightWallet::new(seed_phrase.clone(), &lc, 0).unwrap(); - - // Test the addresses against https://iancoleman.io/bip39/ - let (taddr, pk) = &wallet.get_t_secret_keys()[0]; - assert_eq!(taddr, "t1eQ63fwkQ4n4Eo5uCrPGaAV8FWB2tmx7ui"); - assert_eq!(pk, "Kz9ybX4giKag4NtnP1pi8WQF2B2hZDkFU85S7Dciz3UUhM59AnhE"); - - // Test a couple more - wallet.add_taddr(); - let (taddr, pk) = &wallet.get_t_secret_keys()[1]; - assert_eq!(taddr, "t1NoS6ZgaUTpmjkge2cVpXGcySasdYDrXqh"); - assert_eq!(pk, "KxdmS38pxskS6bbKX43zhTu8ppWckNmWjKsQFX1hwidvhRRgRd3c"); - - let (zaddr, sk) = &wallet.get_z_private_keys()[0]; - assert_eq!(zaddr, "zs1q6xk3q783t5k92kjqt2rkuuww8pdw2euzy5rk6jytw97enx8fhpazdv3th4xe7vsk6e9sfpawfg"); - assert_eq!(sk, "secret-extended-key-main1qvpa0qr8qqqqpqxn4l054nzxpxzp3a8r2djc7sekdek5upce8mc2j2z0arzps4zv940qeg706hd0wq6g5snzvhp332y6vhwyukdn8dhekmmsk7fzvzkqm6ypc99uy63tpesqwxhpre78v06cx8k5xpp9mrhtgqs5dvp68cqx2yrvthflmm2ynl8c0506dekul0f6jkcdmh0292lpphrksyc5z3pxwws97zd5els3l2mjt2s7hntap27mlmt6w0drtfmz36vz8pgu7ec0twfrq"); - - assert_eq!(seed_phrase, Some(wallet.get_seed_phrase())); - } - - #[test] - fn test_lock_unlock() { - const AMOUNT: u64 = 500000; - - let (mut wallet, _, _) = get_test_wallet(AMOUNT); - let config = wallet.config.clone(); - - // Add some addresses - let zaddr0 = encode_payment_address(config.hrp_sapling_address(), - &wallet.extfvks.read().unwrap()[0].default_address().unwrap().1); - let zaddr1 = wallet.add_zaddr(); - let zaddr2 = wallet.add_zaddr(); - - let taddr0 = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); - let taddr1 = wallet.add_taddr(); - let taddr2 = wallet.add_taddr(); - - let seed = wallet.seed; - - // Trying to lock a wallet that's not encrpyted is an error - assert!(wallet.lock().is_err()); - - // Encrypt the wallet - wallet.encrypt("somepassword".to_string()).unwrap(); - - // Encrypting an already encrypted wallet should fail - assert!(wallet.encrypt("somepassword".to_string()).is_err()); - - // Serialize a locked wallet - let mut serialized_data = vec![]; - wallet.write(&mut serialized_data).expect("Serialize wallet"); - - // Should fail when there's a wrong password - assert!(wallet.unlock("differentpassword".to_string()).is_err()); - - // Properly unlock - wallet.unlock("somepassword".to_string()).unwrap(); - - assert_eq!(seed, wallet.seed); - { - let extsks = wallet.extsks.read().unwrap(); - let tkeys = wallet.tkeys.read().unwrap(); - assert_eq!(extsks.len(), 3); - assert_eq!(tkeys.len(), 3); - - assert_eq!(zaddr0, encode_payment_address(config.hrp_sapling_address(), - &ExtendedFullViewingKey::from(&extsks[0]).default_address().unwrap().1)); - assert_eq!(zaddr1, encode_payment_address(config.hrp_sapling_address(), - &ExtendedFullViewingKey::from(&extsks[1]).default_address().unwrap().1)); - assert_eq!(zaddr2, encode_payment_address(config.hrp_sapling_address(), - &ExtendedFullViewingKey::from(&extsks[2]).default_address().unwrap().1)); - - assert_eq!(taddr0, wallet.address_from_sk(&tkeys[0])); - assert_eq!(taddr1, wallet.address_from_sk(&tkeys[1])); - assert_eq!(taddr2, wallet.address_from_sk(&tkeys[2])); - } - - // Unlocking an already unlocked wallet should fail - assert!(wallet.unlock("somepassword".to_string()).is_err()); - - // Trying to serialize a encrypted but unlocked wallet should fail - assert!(wallet.write(&mut vec![]).is_err()); - - // ...but if we lock it again, it should serialize - wallet.lock().unwrap(); - wallet.write(&mut vec![]).expect("Serialize wallet"); - - // Locking an already locked wallet is an error - assert!(wallet.lock().is_err()); - - // Try from a deserialized, locked wallet - let mut wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap(); - wallet2.unlock("somepassword".to_string()).unwrap(); - - assert_eq!(seed, wallet2.seed); - { - let extsks = wallet2.extsks.read().unwrap(); - let tkeys = wallet2.tkeys.read().unwrap(); - assert_eq!(extsks.len(), 3); - assert_eq!(tkeys.len(), 3); - - assert_eq!(zaddr0, encode_payment_address(wallet2.config.hrp_sapling_address(), - &ExtendedFullViewingKey::from(&extsks[0]).default_address().unwrap().1)); - assert_eq!(zaddr1, encode_payment_address(wallet2.config.hrp_sapling_address(), - &ExtendedFullViewingKey::from(&extsks[1]).default_address().unwrap().1)); - assert_eq!(zaddr2, encode_payment_address(wallet2.config.hrp_sapling_address(), - &ExtendedFullViewingKey::from(&extsks[2]).default_address().unwrap().1)); - - assert_eq!(taddr0, wallet2.address_from_sk(&tkeys[0])); - assert_eq!(taddr1, wallet2.address_from_sk(&tkeys[1])); - assert_eq!(taddr2, wallet2.address_from_sk(&tkeys[2])); - } - } - - #[test] - #[should_panic] - fn test_invalid_bip39_t() { - // Passing a 32-byte seed to bip32 should fail. - let config = get_test_config(); - LightWallet::get_taddr_from_bip39seed(&config, &[0u8; 32], 0); - } - - #[test] - #[should_panic] - fn test_invalid_bip39_z() { - // Passing a 32-byte seed to bip32 should fail. - let config = get_test_config(); - LightWallet::get_zaddr_from_bip39seed(&config, &[0u8; 32], 0); - } - - #[test] - fn test_invalid_scan_blocks() { - const AMOUNT: u64 = 500000; - let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT); - - let prev_hash = add_blocks(&wallet, 2, 1, block_hash).unwrap(); - assert_eq!(wallet.blocks.read().unwrap().len(), 3); - - // Block fails to scan for bad encoding - assert_eq!(wallet.scan_block(&[0; 32]), Err(-1)); - - // Block is invalid height - let new_blk = FakeCompactBlock::new(4, prev_hash); - assert_eq!(wallet.scan_block(&new_blk.as_bytes()), Err(2)); - - // Block is right height, but invalid prev height (for reorgs) - let new_blk = FakeCompactBlock::new(2, BlockHash([0; 32])); - assert_eq!(wallet.scan_block(&new_blk.as_bytes()), Err(2)); - - // Block is right height, but invalid prev height (for reorgs) - let new_blk = FakeCompactBlock::new(3, BlockHash([0; 32])); - assert_eq!(wallet.scan_block(&new_blk.as_bytes()), Err(2)); - - // Then the rest add properly - let _ = add_blocks(&wallet, 3, 2, prev_hash).unwrap(); - assert_eq!(wallet.blocks.read().unwrap().len(), 5); - } - - #[test] - fn test_encrypted_zreceive() { - const AMOUNT1: u64 = 50000; - let password: String = "password".to_string(); - - let (mut wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); - - 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 outgoing_memo = "Outgoing Memo".to_string(); - let fee: u64 = DEFAULT_FEE.try_into().unwrap(); - - let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) = get_sapling_params().unwrap(); - - // Create a tx and send to address - let raw_tx = wallet.send_to_address(branch_id, &ss, &so, - vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); - - // Now that we have the transaction, we'll encrypt the wallet - wallet.encrypt(password.clone()).unwrap(); - - // Scan the tx and make sure it gets added - 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(); - - // Now, full scan the Tx, which should populate the Outgoing Meta data - wallet.scan_full_tx(&sent_tx, 2, 0); - - // Now this new Spent tx should be in, so the note should be marked confirmed spent - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); - assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); - - // The sent tx should generate change - assert_eq!(txs[&sent_txid].notes.len(), 1); - assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee); - assert_eq!(txs[&sent_txid].notes[0].is_change, true); - assert_eq!(txs[&sent_txid].notes[0].spent, None); - assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); - - // Outgoing Metadata - assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); - - assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); - - assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, ext_address); - assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT); - assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); - } - - // 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()); - - // unlock the wallet so we can spend to the second z address - wallet.unlock(password.clone()).unwrap(); - - // Second z address - let zaddr2 = wallet.add_zaddr(); - 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(); - - // Now lock the wallet again - wallet.lock().unwrap(); - - let sent_tx2 = Transaction::read(&raw_tx[..]).unwrap(); - let txid2 = sent_tx2.txid(); - - let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); - cb4.add_tx(&sent_tx2); - wallet.scan_block(&cb4.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx2, 3, 0); - - { - let txs = wallet.txs.read().unwrap(); - let prev_change_value = AMOUNT1 - AMOUNT_SENT - fee; - - // Change note from prev transaction is spent - assert_eq!(txs[&sent_txid].notes[0].note.value, prev_change_value); - assert_eq!(txs[&sent_txid].notes[0].is_change, true); - assert_eq!(txs[&sent_txid].notes[0].spent, Some(txid2)); - - // New change note. So find it. - let change_note = txs[&txid2].notes.iter().find(|n| n.is_change).unwrap(); - - // New incoming tx is present - assert_eq!(change_note.note.value, prev_change_value - (ZAMOUNT2+fee)); - assert_eq!(change_note.spent, None); - assert_eq!(change_note.unconfirmed_spent, None); - - // Find zaddr2 - let zaddr2_note = txs[&txid2].notes.iter().find(|n| n.note.value == ZAMOUNT2).unwrap(); - assert_eq!(zaddr2_note.account, 1); - assert_eq!(zaddr2_note.is_change, false); - assert_eq!(zaddr2_note.spent, None); - assert_eq!(zaddr2_note.unconfirmed_spent, None); - assert_eq!(LightWallet::memo_str(&zaddr2_note.memo), Some(outgoing_memo2)); - } - } - - - #[test] - fn test_encrypted_treceive() { - const AMOUNT1: u64 = 50000; - let password: String = "password".to_string(); - let (mut wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); - - let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); - let (ss, so) = get_sapling_params().unwrap(); - - let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); - 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(); - - // Now that we have the transaction, we'll encrypt the wallet - wallet.encrypt(password.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); - - // Now this new Spent tx should be in, so the note should be marked confirmed spent - { - let txs = wallet.txs.read().unwrap(); - assert_eq!(txs[&txid1].notes.len(), 1); - assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); - assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); - assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); - - // The sent tx should generate change - assert_eq!(txs[&sent_txid].notes.len(), 1); - assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee); - assert_eq!(txs[&sent_txid].notes[0].is_change, true); - assert_eq!(txs[&sent_txid].notes[0].spent, None); - assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); - - // Outgoing Metadata - assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); - assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, taddr); - assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT); - assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); - } - - // 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()); - - // unlock the wallet so we can spend to the second z address - wallet.unlock(password.clone()).unwrap(); - - // Second z address - 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(); - - // Now lock the wallet again - wallet.lock().unwrap(); - - let sent_tx2 = Transaction::read(&raw_tx[..]).unwrap(); - let txid2 = sent_tx2.txid(); - - let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); - cb4.add_tx(&sent_tx2); - wallet.scan_block(&cb4.as_bytes()).unwrap(); - wallet.scan_full_tx(&sent_tx2, 3, 0); - - { - let txs = wallet.txs.read().unwrap(); - let prev_change_value = AMOUNT1 - AMOUNT_SENT - fee; - - // Change note from prev transaction is spent - assert_eq!(txs[&sent_txid].notes[0].note.value, prev_change_value); - assert_eq!(txs[&sent_txid].notes[0].is_change, true); - assert_eq!(txs[&sent_txid].notes[0].spent, Some(txid2)); - - // New change note. So find it. - let change_note = txs[&txid2].notes.iter().find(|n| n.is_change).unwrap(); - - // New incoming tx is present - assert_eq!(change_note.note.value, prev_change_value - (TAMOUNT2+fee)); - assert_eq!(change_note.spent, None); - assert_eq!(change_note.unconfirmed_spent, None); - - // Find taddr2 - let utxo2 = txs[&txid2].utxos.iter().find(|u| u.value == TAMOUNT2).unwrap(); - assert_eq!(txs[&txid2].utxos.len(), 1); - assert_eq!(utxo2.address, taddr2); - assert_eq!(utxo2.txid, txid2); - assert_eq!(utxo2.spent, None); - assert_eq!(utxo2.unconfirmed_spent, None); - } - } - -} \ No newline at end of file +pub mod tests; \ No newline at end of file diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs new file mode 100644 index 0000000..3199c2d --- /dev/null +++ b/lib/src/lightwallet/tests.rs @@ -0,0 +1,1959 @@ +use std::convert::TryInto; +use std::io::{Error}; +use rand::{RngCore, rngs::OsRng}; + +use ff::{Field, PrimeField, PrimeFieldRepr}; +use pairing::bls12_381::Bls12; +use protobuf::{Message, UnknownFields, CachedSize, RepeatedField}; +use zcash_client_backend::{encoding::encode_payment_address, + proto::compact_formats::{ + CompactBlock, CompactOutput, CompactSpend, CompactTx, + } +}; +use zcash_primitives::{ + block::BlockHash, + jubjub::fs::Fs, + note_encryption::{Memo, SaplingNoteEncryption}, + primitives::{Note, PaymentAddress}, + legacy::{Script, TransparentAddress,}, + transaction::{ + TxId, Transaction, TransactionData, + components::{TxOut, TxIn, OutPoint, Amount,}, + components::amount::DEFAULT_FEE, + }, + zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, + JUBJUB, +}; + +use sha2::{Sha256, Digest}; + +use super::LightWallet; +use super::LightClientConfig; +use secp256k1::{Secp256k1, key::PublicKey, key::SecretKey}; +use crate::SaplingParams; + +fn get_sapling_params() -> Result<(Vec, Vec), Error> { + // Read Sapling Params + let mut sapling_output = vec![]; + sapling_output.extend_from_slice(SaplingParams::get("sapling-output.params").unwrap().as_ref()); + println!("Read output {}", sapling_output.len()); + + let mut sapling_spend = vec![]; + sapling_spend.extend_from_slice(SaplingParams::get("sapling-spend.params").unwrap().as_ref()); + println!("Read output {}", sapling_spend.len()); + + Ok((sapling_spend, sapling_output)) +} + +struct FakeCompactBlock { + block: CompactBlock, +} + +impl FakeCompactBlock { + fn new(height: i32, prev_hash: BlockHash) -> Self { + // Create a fake Note for the account + let mut rng = OsRng; + + let mut cb = CompactBlock::new(); + + cb.set_height(height as u64); + cb.hash.resize(32, 0); + rng.fill_bytes(&mut cb.hash); + + cb.prevHash.extend_from_slice(&prev_hash.0); + + FakeCompactBlock { block: cb } + } + + fn as_bytes(&self) -> Vec { + self.block.write_to_bytes().unwrap() + } + + fn hash(&self) -> BlockHash { + BlockHash(self.block.hash[..].try_into().unwrap()) + } + + fn tx_to_compact_tx(tx: &Transaction, index: u64) -> CompactTx { + let spends = tx.shielded_spends.iter().map(|s| { + let mut c_spend = CompactSpend::default(); + c_spend.set_nf(s.nullifier.to_vec()); + + c_spend + }).collect::>(); + + let outputs = tx.shielded_outputs.iter().map(|o| { + let mut c_out = CompactOutput::default(); + + let mut cmu_bytes = vec![]; + o.cmu.into_repr().write_le(&mut cmu_bytes).unwrap(); + + let mut epk_bytes = vec![]; + o.ephemeral_key.write(&mut epk_bytes).unwrap(); + + c_out.set_cmu(cmu_bytes); + c_out.set_epk(epk_bytes); + c_out.set_ciphertext(o.enc_ciphertext[0..52].to_vec()); + + c_out + }).collect::>(); + + CompactTx { + index, + hash: tx.txid().0.to_vec(), + fee: 0, // TODO: Get Fee + spends: RepeatedField::from_vec(spends), + outputs: RepeatedField::from_vec(outputs), + unknown_fields: UnknownFields::default(), + cached_size: CachedSize::default(), + } + } + + // Convert the transaction into a CompactTx and add it to this block + fn add_tx(&mut self, tx: &Transaction) { + let ctx = FakeCompactBlock::tx_to_compact_tx(&tx, self.block.vtx.len() as u64); + self.block.vtx.push(ctx); + } + + // Add a new tx into the block, paying the given address the amount. + // Returns the nullifier of the new note. + fn add_tx_paying(&mut self, extfvk: ExtendedFullViewingKey, value: u64) + -> (Vec, TxId) { + let to = extfvk.default_address().unwrap().1; + let value = Amount::from_u64(value).unwrap(); + + // Create a fake Note for the account + let mut rng = OsRng; + let note = Note { + g_d: to.diversifier.g_d::(&JUBJUB).unwrap(), + pk_d: to.pk_d.clone(), + value: value.into(), + r: Fs::random(&mut rng), + }; + let encryptor = SaplingNoteEncryption::new( + extfvk.fvk.ovk, + note.clone(), + to.clone(), + Memo::default(), + &mut rng, + ); + let mut cmu = vec![]; + note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap(); + let mut epk = vec![]; + encryptor.epk().write(&mut epk).unwrap(); + let enc_ciphertext = encryptor.encrypt_note_plaintext(); + + // Create a fake CompactBlock containing the note + let mut cout = CompactOutput::new(); + cout.set_cmu(cmu); + cout.set_epk(epk); + cout.set_ciphertext(enc_ciphertext[..52].to_vec()); + let mut ctx = CompactTx::new(); + let mut txid = vec![0; 32]; + rng.fill_bytes(&mut txid); + ctx.set_hash(txid.clone()); + ctx.outputs.push(cout); + + self.block.vtx.push(ctx); + (note.nf(&extfvk.fvk.vk, 0, &JUBJUB), TxId(txid[..].try_into().unwrap())) + } + + fn add_tx_spending(&mut self, + (nf, in_value): (Vec, u64), + extfvk: ExtendedFullViewingKey, + to: PaymentAddress, + value: u64) -> TxId { + let mut rng = OsRng; + + let in_value = Amount::from_u64(in_value).unwrap(); + let value = Amount::from_u64(value).unwrap(); + + // Create a fake CompactBlock containing the note + let mut cspend = CompactSpend::new(); + cspend.set_nf(nf); + let mut ctx = CompactTx::new(); + let mut txid = vec![0; 32]; + rng.fill_bytes(&mut txid); + ctx.set_hash(txid.clone()); + ctx.spends.push(cspend); + + // Create a fake Note for the payment + ctx.outputs.push({ + let note = Note { + g_d: to.diversifier.g_d::(&JUBJUB).unwrap(), + pk_d: to.pk_d.clone(), + value: value.into(), + r: Fs::random(&mut rng), + }; + let encryptor = SaplingNoteEncryption::new( + extfvk.fvk.ovk, + note.clone(), + to, + Memo::default(), + &mut rng, + ); + let mut cmu = vec![]; + note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap(); + let mut epk = vec![]; + encryptor.epk().write(&mut epk).unwrap(); + let enc_ciphertext = encryptor.encrypt_note_plaintext(); + + let mut cout = CompactOutput::new(); + cout.set_cmu(cmu); + cout.set_epk(epk); + cout.set_ciphertext(enc_ciphertext[..52].to_vec()); + cout + }); + + // Create a fake Note for the change + ctx.outputs.push({ + let change_addr = extfvk.default_address().unwrap().1; + let note = Note { + g_d: change_addr.diversifier.g_d::(&JUBJUB).unwrap(), + pk_d: change_addr.pk_d.clone(), + value: (in_value - value).into(), + r: Fs::random(&mut rng), + }; + let encryptor = SaplingNoteEncryption::new( + extfvk.fvk.ovk, + note.clone(), + change_addr, + Memo::default(), + &mut rng, + ); + let mut cmu = vec![]; + note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap(); + let mut epk = vec![]; + encryptor.epk().write(&mut epk).unwrap(); + let enc_ciphertext = encryptor.encrypt_note_plaintext(); + + let mut cout = CompactOutput::new(); + cout.set_cmu(cmu); + cout.set_epk(epk); + cout.set_ciphertext(enc_ciphertext[..52].to_vec()); + cout + }); + + self.block.vtx.push(ctx); + + TxId(txid[..].try_into().unwrap()) + } +} + +struct FakeTransaction { + tx: Transaction, +} + +impl FakeTransaction { + // New FakeTransaction with random txid + fn new(rng: &mut R) -> Self { + let mut txid = [0u8; 32]; + rng.fill_bytes(&mut txid); + FakeTransaction::new_with_txid(TxId(txid)) + } + + fn new_with_txid(txid: TxId) -> Self { + FakeTransaction { + tx: Transaction { + txid, + data: TransactionData::new() + } + } + } + + fn get_tx(&self) -> &Transaction { + &self.tx + } + + fn add_t_output(&mut self, pk: &PublicKey, value: u64) { + let mut hash160 = ripemd160::Ripemd160::new(); + hash160.input(Sha256::digest(&pk.serialize()[..].to_vec())); + + let taddr_bytes = hash160.result(); + + self.tx.data.vout.push(TxOut { + value: Amount::from_u64(value).unwrap(), + script_pubkey: TransparentAddress::PublicKey(taddr_bytes.try_into().unwrap()).script(), + }); + } + + fn add_t_input(&mut self, txid: TxId, n: u32) { + self.tx.data.vin.push(TxIn { + prevout: OutPoint{ + hash: txid.0, + n + }, + script_sig: Script{0: vec![]}, + sequence: 0, + }); + } +} + +#[test] +fn test_z_balances() { + let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); + + const AMOUNT1:u64 = 5; + // 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)); + + let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); + cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); + + // Make sure that the intial state is empty + assert_eq!(wallet.txs.read().unwrap().len(), 0); + assert_eq!(wallet.blocks.read().unwrap().len(), 0); + assert_eq!(wallet.zbalance(None), 0); + assert_eq!(wallet.zbalance(address.clone()), 0); + + wallet.scan_block(&cb1.as_bytes()).unwrap(); + + assert_eq!(wallet.txs.read().unwrap().len(), 1); + assert_eq!(wallet.blocks.read().unwrap().len(), 1); + assert_eq!(wallet.zbalance(None), AMOUNT1); + assert_eq!(wallet.zbalance(address.clone()), AMOUNT1); + + const AMOUNT2:u64 = 10; + + // Add a second block + let mut cb2 = FakeCompactBlock::new(1, cb1.hash()); + cb2.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT2); + + wallet.scan_block(&cb2.as_bytes()).unwrap(); + + assert_eq!(wallet.txs.read().unwrap().len(), 2); + assert_eq!(wallet.blocks.read().unwrap().len(), 2); + assert_eq!(wallet.zbalance(None), AMOUNT1 + AMOUNT2); + assert_eq!(wallet.zbalance(address.clone()), AMOUNT1 + AMOUNT2); +} + +#[test] +fn test_z_change_balances() { + let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); + + // First, add an incoming transaction + const AMOUNT1:u64 = 5; + + let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); + let (nf1, txid1) = cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); + + wallet.scan_block(&cb1.as_bytes()).unwrap(); + + assert_eq!(wallet.txs.read().unwrap().len(), 1); + assert_eq!(wallet.blocks.read().unwrap().len(), 1); + assert_eq!(wallet.zbalance(None), AMOUNT1); + + const AMOUNT2:u64 = 2; + + // Add a second block, spending the first note + 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); + wallet.scan_block(&cb2.as_bytes()).unwrap(); + + // Now, the original note should be spent and there should be a change + assert_eq!(wallet.zbalance(None), AMOUNT1 - AMOUNT2); + + let txs = wallet.txs.read().unwrap(); + + // Old note was spent + assert_eq!(txs[&txid1].txid, txid1); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].spent.unwrap(), txid2); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].is_change, false); + + // new note is not spent + assert_eq!(txs[&txid2].txid, txid2); + assert_eq!(txs[&txid2].notes.len(), 1); + assert_eq!(txs[&txid2].notes[0].spent, None); + assert_eq!(txs[&txid2].notes[0].note.value, AMOUNT1 - AMOUNT2); + assert_eq!(txs[&txid2].notes[0].is_change, true); + assert_eq!(txs[&txid2].total_shielded_value_spent, AMOUNT1); +} + +#[test] +fn test_t_receive_spend() { + let mut rng = OsRng; + let secp = Secp256k1::new(); + + let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); + + let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); + let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); + + const AMOUNT1: u64 = 20; + + let mut tx = FakeTransaction::new(&mut rng); + tx.add_t_output(&pk, AMOUNT1); + let txid1 = tx.get_tx().txid(); + + wallet.scan_full_tx(&tx.get_tx(), 100, 0); // Pretend it is at height 100 + + { + let txs = wallet.txs.read().unwrap(); + + // Now make sure the t addr was recieved + assert_eq!(txs.len(), 1); + assert_eq!(txs[&txid1].utxos.len(), 1); + assert_eq!(txs[&txid1].utxos[0].address, taddr); + assert_eq!(txs[&txid1].utxos[0].txid, txid1); + assert_eq!(txs[&txid1].utxos[0].output_index, 0); + assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1); + assert_eq!(txs[&txid1].utxos[0].height, 100); + assert_eq!(txs[&txid1].utxos[0].spent, None); + assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None); + + assert_eq!(wallet.tbalance(None), AMOUNT1); + assert_eq!(wallet.tbalance(Some(taddr)), AMOUNT1); + } + + // Create a new Tx, spending this taddr + let mut tx = FakeTransaction::new(&mut rng); + tx.add_t_input(txid1, 0); + let txid2 = tx.get_tx().txid(); + + wallet.scan_full_tx(&tx.get_tx(), 101, 0); // Pretent it is at height 101 + + { + // Make sure the txid was spent + let txs = wallet.txs.read().unwrap(); + + // Old utxo, that should be spent now + assert_eq!(txs.len(), 2); + assert_eq!(txs[&txid1].utxos.len(), 1); + assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1); + assert_eq!(txs[&txid1].utxos[0].spent, Some(txid2)); + assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None); + + assert_eq!(txs[&txid2].block, 101); // The second TxId is at block 101 + assert_eq!(txs[&txid2].utxos.len(), 0); // The second TxId has no UTXOs + assert_eq!(txs[&txid2].total_transparent_value_spent, AMOUNT1); + + // Make sure there is no t-ZEC left + assert_eq!(wallet.tbalance(None), 0); + } +} + + +#[test] +/// This test spends and receives t addresses among non-wallet t addresses to make sure that +/// we're detecting and spending only our t addrs. +fn test_t_receive_spend_among_tadds() { + let mut rng = OsRng; + let secp = Secp256k1::new(); + + let wallet = LightWallet::new(None, &get_test_config(), 0).unwrap(); + + let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); + let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); + + let non_wallet_sk = &SecretKey::from_slice(&[1u8; 32]).unwrap(); + let non_wallet_pk = PublicKey::from_secret_key(&secp, &non_wallet_sk); + + const AMOUNT1: u64 = 30; + + let mut tx = FakeTransaction::new(&mut rng); + // Add a non-wallet output + tx.add_t_output(&non_wallet_pk, 20); + tx.add_t_output(&pk, AMOUNT1); // Our wallet t output + tx.add_t_output(&non_wallet_pk, 25); + let txid1 = tx.get_tx().txid(); + + wallet.scan_full_tx(&tx.get_tx(), 100, 0); // Pretend it is at height 100 + + { + let txs = wallet.txs.read().unwrap(); + + // Now make sure the t addr was received + assert_eq!(txs.len(), 1); + assert_eq!(txs[&txid1].utxos.len(), 1); + assert_eq!(txs[&txid1].utxos[0].address, taddr); + assert_eq!(txs[&txid1].utxos[0].txid, txid1); + assert_eq!(txs[&txid1].utxos[0].output_index, 1); + assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1); + assert_eq!(txs[&txid1].utxos[0].height, 100); + assert_eq!(txs[&txid1].utxos[0].spent, None); + assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None); + + assert_eq!(wallet.tbalance(None), AMOUNT1); + assert_eq!(wallet.tbalance(Some(taddr)), AMOUNT1); + } + + // Create a new Tx, spending this taddr + let mut tx = FakeTransaction::new(&mut rng); + tx.add_t_input(txid1, 1); // Ours was at position 1 in the input tx + let txid2 = tx.get_tx().txid(); + + wallet.scan_full_tx(&tx.get_tx(), 101, 0); // Pretent it is at height 101 + + { + // Make sure the txid was spent + let txs = wallet.txs.read().unwrap(); + + // Old utxo, that should be spent now + assert_eq!(txs.len(), 2); + assert_eq!(txs[&txid1].utxos.len(), 1); + assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1); + assert_eq!(txs[&txid1].utxos[0].spent, Some(txid2)); + assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None); + + assert_eq!(txs[&txid2].block, 101); // The second TxId is at block 101 + assert_eq!(txs[&txid2].utxos.len(), 0); // The second TxId has no UTXOs + assert_eq!(txs[&txid2].total_transparent_value_spent, AMOUNT1); + + // Make sure there is no t-ZEC left + assert_eq!(wallet.tbalance(None), 0); + } +} + +#[test] +fn test_serialization() { + let secp = Secp256k1::new(); + let config = get_test_config(); + + let wallet = LightWallet::new(None, &config, 0).unwrap(); + + // First, add an incoming transaction + const AMOUNT1:u64 = 5; + + let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32])); + let (nf1, txid1) = cb1.add_tx_paying(wallet.extfvks.read().unwrap()[0].clone(), AMOUNT1); + + wallet.scan_block(&cb1.as_bytes()).unwrap(); + + assert_eq!(wallet.txs.read().unwrap().len(), 1); + assert_eq!(wallet.blocks.read().unwrap().len(), 1); + assert_eq!(wallet.zbalance(None), AMOUNT1); + + // Add a t input at the Tx + let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); + let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); + + const TAMOUNT1: u64 = 20; + + let mut tx = FakeTransaction::new_with_txid(txid1); + tx.add_t_output(&pk, TAMOUNT1); + wallet.scan_full_tx(&tx.get_tx(), 0, 0); // Height 0 + + const AMOUNT2:u64 = 2; + + // Add a second block, spending the first note + 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); + wallet.scan_block(&cb2.as_bytes()).unwrap(); + + let mut tx = FakeTransaction::new_with_txid(txid2); + tx.add_t_input(txid1, 0); + wallet.scan_full_tx(&tx.get_tx(), 1, 0); // Height 1 + + // Now, the original note should be spent and there should be a change + assert_eq!(wallet.zbalance(None), AMOUNT1 - AMOUNT2 ); // The t addr amount is received + spent, so it cancels out + + // Now, serialize the wallet and read it back again + let mut serialized_data = vec![]; + wallet.write(&mut serialized_data).expect("Serialize wallet"); + let wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap(); + + assert_eq!(wallet2.zbalance(None), AMOUNT1 - AMOUNT2); + + // Test the keys were serialized correctly + { + 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.tkeys.read().unwrap().len(), wallet2.tkeys.read().unwrap().len()); + assert_eq!(wallet.tkeys.read().unwrap()[0], wallet2.tkeys.read().unwrap()[0]); + } + + // Test blocks were serialized properly + { + let blks = wallet2.blocks.read().unwrap(); + + assert_eq!(blks.len(), 2); + assert_eq!(blks[0].height, 0); + assert_eq!(blks[1].height, 1); + } + + // Test txns were serialized properly. + { + let txs = wallet2.txs.read().unwrap(); + + // Old note was spent + assert_eq!(txs[&txid1].txid, txid1); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].spent.unwrap(), txid2); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].is_change, false); + + // Old UTXO was spent + assert_eq!(txs[&txid1].utxos.len(), 1); + assert_eq!(txs[&txid1].utxos[0].address, taddr); + assert_eq!(txs[&txid1].utxos[0].txid, txid1); + assert_eq!(txs[&txid1].utxos[0].output_index, 0); + assert_eq!(txs[&txid1].utxos[0].value, TAMOUNT1); + assert_eq!(txs[&txid1].utxos[0].height, 0); + assert_eq!(txs[&txid1].utxos[0].spent, Some(txid2)); + assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None); + + // new note is not spent + assert_eq!(txs[&txid2].txid, txid2); + assert_eq!(txs[&txid2].notes.len(), 1); + assert_eq!(txs[&txid2].notes[0].spent, None); + assert_eq!(txs[&txid2].notes[0].note.value, AMOUNT1 - AMOUNT2); + assert_eq!(txs[&txid2].notes[0].is_change, true); + assert_eq!(txs[&txid2].total_shielded_value_spent, AMOUNT1); + + // The UTXO was spent in txid2 + assert_eq!(txs[&txid2].utxos.len(), 0); // The second TxId has no UTXOs + assert_eq!(txs[&txid2].total_transparent_value_spent, TAMOUNT1); + } +} + +#[test] +fn test_multi_serialization() { + let config = get_test_config(); + + let wallet = LightWallet::new(None, &config, 0).unwrap(); + + let taddr1 = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); + let taddr2 = wallet.add_taddr(); + + let (zaddr1, zpk1) = &wallet.get_z_private_keys()[0]; + let zaddr2 = wallet.add_zaddr(); + + let mut serialized_data = vec![]; + wallet.write(&mut serialized_data).expect("Serialize wallet"); + let wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap(); + + assert_eq!(wallet2.tkeys.read().unwrap().len(), 2); + assert_eq!(wallet2.extsks.read().unwrap().len(), 2); + assert_eq!(wallet2.extfvks.read().unwrap().len(), 2); + assert_eq!(wallet2.zaddress.read().unwrap().len(), 2); + + assert_eq!(taddr1, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0])); + assert_eq!(taddr2, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[1])); + + let (w2_zaddr1, w2_zpk1) = &wallet.get_z_private_keys()[0]; + let (w2_zaddr2, _) = &wallet.get_z_private_keys()[1]; + assert_eq!(zaddr1, w2_zaddr1); + assert_eq!(zpk1, w2_zpk1); + assert_eq!(zaddr2, *w2_zaddr2); + +} + +fn get_test_config() -> LightClientConfig { + LightClientConfig { + server: "0.0.0.0:0".parse().unwrap(), + chain_name: "test".to_string(), + sapling_activation_height: 0, + consensus_branch_id: "000000".to_string(), + anchor_offset: 0, + no_cert_verification: false, + } +} + +// Get a test wallet already setup with a single note +fn get_test_wallet(amount: u64) -> (LightWallet, TxId, BlockHash) { + let config = get_test_config(); + + 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); + wallet.scan_block(&cb1.as_bytes()).unwrap(); + + // We have one note + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, amount); + assert_eq!(txs[&txid1].notes[0].spent, None); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + } + + assert_eq!(wallet.verified_zbalance(None), amount); + + // Create a new block so that the note is now verified to be spent + let cb2 = FakeCompactBlock::new(1, cb1.hash()); + wallet.scan_block(&cb2.as_bytes()).unwrap(); + + (wallet, txid1, cb2.hash()) +} + +#[test] +fn test_z_spend() { + const AMOUNT1: u64 = 50000; + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); + + 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 outgoing_memo = "Outgoing Memo".to_string(); + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + // Create a tx and send to address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); + + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid = sent_tx.txid(); + + // Now, the note should be unconfirmed spent + { + let txs = wallet.txs.read().unwrap(); + + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].is_change, false); + assert_eq!(txs[&txid1].notes[0].spent, None); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, Some(sent_txid)); + } + + let mut cb3 = FakeCompactBlock::new(2, block_hash); + cb3.add_tx(&sent_tx); + wallet.scan_block(&cb3.as_bytes()).unwrap(); + + // Now this new Spent tx should be in, so the note should be marked confirmed spent + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + + // The sent tx should generate change + assert_eq!(txs[&sent_txid].notes.len(), 1); + assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, None); + assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); + } + + // Now, full scan the Tx, which should populate the Outgoing Meta data + wallet.scan_full_tx(&sent_tx, 2, 0); + + // Check Outgoing Metadata + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); + + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); + + assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, ext_address); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); + } +} + +#[test] +fn test_multi_z() { + const AMOUNT1: u64 = 50000; + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); + + let zaddr2 = wallet.add_zaddr(); + + const AMOUNT_SENT: u64 = 20; + + let outgoing_memo = "Outgoing Memo".to_string(); + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) =get_sapling_params().unwrap(); + + // Create a tx and send to address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + 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); + + // Because the builder will randomize notes outputted, we need to find + // which note number is the change and which is the output note (Because this tx + // had both outputs in the same Tx) + let (change_note_number, ext_note_number) = { + let txs = wallet.txs.read().unwrap(); + if txs[&sent_txid].notes[0].is_change { (0,1) } else { (1,0) } + }; + + // Now this new Spent tx should be in, so the note should be marked confirmed spent + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + + // The sent tx should generate change + the new incoming note + assert_eq!(txs[&sent_txid].notes.len(), 2); + + assert_eq!(txs[&sent_txid].notes[change_note_number].note.value, AMOUNT1 - AMOUNT_SENT - fee); + assert_eq!(txs[&sent_txid].notes[change_note_number].account, 0); + assert_eq!(txs[&sent_txid].notes[change_note_number].is_change, true); + assert_eq!(txs[&sent_txid].notes[change_note_number].spent, None); + assert_eq!(txs[&sent_txid].notes[change_note_number].unconfirmed_spent, None); + assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[change_note_number].memo), None); + + assert_eq!(txs[&sent_txid].notes[ext_note_number].note.value, AMOUNT_SENT); + assert_eq!(txs[&sent_txid].notes[ext_note_number].account, 1); + assert_eq!(txs[&sent_txid].notes[ext_note_number].is_change, false); + assert_eq!(txs[&sent_txid].notes[ext_note_number].spent, None); + assert_eq!(txs[&sent_txid].notes[ext_note_number].unconfirmed_spent, None); + assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[ext_note_number].memo), Some(outgoing_memo)); + + assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); + + // No Outgoing meta data, since this is a wallet -> wallet tx + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 0); + } + + // Now spend the money, which should pick notes from both addresses + let amount_all:u64 = (AMOUNT1 - AMOUNT_SENT - fee) + (AMOUNT_SENT) - fee; + let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); + + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr, amount_all, None)]).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_ext_txid = sent_tx.txid(); + + let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); + cb4.add_tx(&sent_tx); + wallet.scan_block(&cb4.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx, 3, 0); + + { + // Both notes should be spent now. + let txs = wallet.txs.read().unwrap(); + + assert_eq!(txs[&sent_txid].notes[change_note_number].is_change, true); + assert_eq!(txs[&sent_txid].notes[change_note_number].spent, Some(sent_ext_txid)); + assert_eq!(txs[&sent_txid].notes[change_note_number].unconfirmed_spent, None); + + assert_eq!(txs[&sent_txid].notes[ext_note_number].is_change, false); + assert_eq!(txs[&sent_txid].notes[ext_note_number].spent, Some(sent_ext_txid)); + assert_eq!(txs[&sent_txid].notes[ext_note_number].unconfirmed_spent, None); + + // Check outgoing metadata for the external sent tx + assert_eq!(txs[&sent_ext_txid].notes.len(), 0); // No change was generated + assert_eq!(txs[&sent_ext_txid].outgoing_metadata.len(), 1); + assert_eq!(txs[&sent_ext_txid].outgoing_metadata[0].address, taddr); + assert_eq!(txs[&sent_ext_txid].outgoing_metadata[0].value, amount_all); + } +} + +#[test] +fn test_z_spend_to_taddr() { + const AMOUNT1: u64 = 50000; + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); + const AMOUNT_SENT: u64 = 30; + 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 sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid = sent_tx.txid(); + + // Now, the note should be unconfirmed spent + { + let txs = wallet.txs.read().unwrap(); + + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].is_change, false); + assert_eq!(txs[&txid1].notes[0].spent, None); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, Some(sent_txid)); + } + + let mut cb3 = FakeCompactBlock::new(2, block_hash); + cb3.add_tx(&sent_tx); + wallet.scan_block(&cb3.as_bytes()).unwrap(); + + + // Now this new Spent tx should be in, so the note should be marked confirmed spent + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + + // The sent tx should generate change + assert_eq!(txs[&sent_txid].notes.len(), 1); + assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, None); + assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); + } + + // Now, full scan the Tx, which should populate the Outgoing Meta data + wallet.scan_full_tx(&sent_tx, 2, 0); + + // Check Outgoing Metadata for t address + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, taddr); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT); + assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); + } +} + +#[test] +fn test_t_spend_to_z() { + let mut rng = OsRng; + let secp = Secp256k1::new(); + + const AMOUNT_Z: u64 = 50000; + const AMOUNT_T: u64 = 40000; + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT_Z); + + let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0]); + let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); + + let mut tx = FakeTransaction::new(&mut rng); + tx.add_t_output(&pk, AMOUNT_T); + let txid_t = tx.get_tx().txid(); + + wallet.scan_full_tx(&tx.get_tx(), 1, 0); // Pretend it is at height 1 + + { + let txs = wallet.txs.read().unwrap(); + + // Now make sure the t addr was recieved + assert_eq!(txs[&txid_t].utxos.len(), 1); + assert_eq!(txs[&txid_t].utxos[0].address, taddr); + assert_eq!(txs[&txid_t].utxos[0].spent, None); + assert_eq!(txs[&txid_t].utxos[0].unconfirmed_spent, None); + + assert_eq!(wallet.tbalance(None), AMOUNT_T); + } + + + 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 outgoing_memo = "Outgoing Memo".to_string(); + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) =get_sapling_params().unwrap(); + + // Create a tx and send to address. 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 sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid = sent_tx.txid(); + + // Verify the sent_tx for sanity + { + // The tx has 1 note spent, 1 utxo spent, and (1 note out, 1 note change) + assert_eq!(sent_tx.shielded_spends.len(), 1); + assert_eq!(sent_tx.vin.len(), 1); + assert_eq!(sent_tx.shielded_outputs.len(), 2); + } + + // Now, the note and utxo should be unconfirmed spent + { + let txs = wallet.txs.read().unwrap(); + + // UTXO + assert_eq!(txs[&txid_t].utxos.len(), 1); + assert_eq!(txs[&txid_t].utxos[0].address, taddr); + assert_eq!(txs[&txid_t].utxos[0].spent, None); + assert_eq!(txs[&txid_t].utxos[0].unconfirmed_spent, Some(sent_txid)); + + // Note + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT_Z); + assert_eq!(txs[&txid1].notes[0].spent, None); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, Some(sent_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); + + // 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, AMOUNT_Z); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + + // The UTXO should also be spent + assert_eq!(txs[&txid_t].utxos[0].address, taddr); + assert_eq!(txs[&txid_t].utxos[0].spent, Some(sent_txid)); + assert_eq!(txs[&txid_t].utxos[0].unconfirmed_spent, None); + + // The sent tx should generate change + assert_eq!(txs[&sent_txid].notes.len(), 1); + assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT_Z + AMOUNT_T - AMOUNT_SENT - fee); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, None); + assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); + } +} + + #[test] +fn test_z_incoming_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 memo = "Incoming Memo".to_string(); + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + // Create a tx and send to address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + 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(memo)); + } +} + + #[test] +fn test_z_to_t_withinwallet() { + const AMOUNT: u64 = 500000; + const AMOUNT_SENT: u64 = 20000; + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT); + + let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); + + 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![(&taddr, AMOUNT_SENT, None)]).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(); + + // We have the original note + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT); + + // We have the spent tx + assert_eq!(txs[&sent_txid].notes.len(), 1); + assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT - AMOUNT_SENT - fee); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, None); + assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); + + // Since we sent the Tx to ourself, there should be no outgoing + // metadata + assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT); + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 0); + + + // We have the taddr utxo in the same Tx + assert_eq!(txs[&sent_txid].utxos.len(), 1); + assert_eq!(txs[&sent_txid].utxos[0].address, taddr); + assert_eq!(txs[&sent_txid].utxos[0].value, AMOUNT_SENT); + assert_eq!(txs[&sent_txid].utxos[0].spent, None); + assert_eq!(txs[&sent_txid].utxos[0].unconfirmed_spent, None); + + } +} + + #[test] +fn test_multi_t() { + const AMOUNT: u64 = 5000000; + const AMOUNT_SENT1: u64 = 20000; + const AMOUNT_SENT2: u64 = 10000; + + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT); + + // Add a new taddr + let taddr2 = wallet.add_taddr(); + + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + // Create a Tx and send to the second t address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr2, AMOUNT_SENT1, None)]).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid1 = sent_tx.txid(); + + // Add it to a block + let mut cb3 = FakeCompactBlock::new(2, block_hash); + cb3.add_tx(&sent_tx); + wallet.scan_block(&cb3.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx, 2, 0); + + // Check that the send to the second taddr worked + { + let txs = wallet.txs.read().unwrap(); + + // We have the original note + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT); + + // We have the spent tx + assert_eq!(txs[&sent_txid1].notes.len(), 1); + assert_eq!(txs[&sent_txid1].notes[0].note.value, AMOUNT - AMOUNT_SENT1 - fee); + assert_eq!(txs[&sent_txid1].notes[0].is_change, true); + assert_eq!(txs[&sent_txid1].notes[0].spent, None); + assert_eq!(txs[&sent_txid1].notes[0].unconfirmed_spent, None); + + // Since we sent the Tx to ourself, there should be no outgoing + // metadata + assert_eq!(txs[&sent_txid1].total_shielded_value_spent, AMOUNT); + assert_eq!(txs[&sent_txid1].outgoing_metadata.len(), 0); + + + // We have the taddr utxo in the same Tx + assert_eq!(txs[&sent_txid1].utxos.len(), 1); + assert_eq!(txs[&sent_txid1].utxos[0].address, taddr2); + assert_eq!(txs[&sent_txid1].utxos[0].value, AMOUNT_SENT1); + assert_eq!(txs[&sent_txid1].utxos[0].spent, None); + assert_eq!(txs[&sent_txid1].utxos[0].unconfirmed_spent, None); + } + + // Send some money to the 3rd t addr + let taddr3 = wallet.add_taddr(); + + // Create a Tx and send to the second t address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&taddr3, AMOUNT_SENT2, None)]).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid2 = sent_tx.txid(); + + // Add it to a block + let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); + cb4.add_tx(&sent_tx); + wallet.scan_block(&cb4.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx, 3, 0); + + // Quickly check we have it + { + let txs = wallet.txs.read().unwrap(); + + // We have the taddr utxo in the same Tx + assert_eq!(txs[&sent_txid2].utxos.len(), 1); + assert_eq!(txs[&sent_txid2].utxos[0].address, taddr3); + assert_eq!(txs[&sent_txid2].utxos[0].value, AMOUNT_SENT2); + + // Old UTXO was spent here + assert_eq!(txs[&sent_txid1].utxos.len(), 1); + assert_eq!(txs[&sent_txid1].utxos[0].value, AMOUNT_SENT1); + assert_eq!(txs[&sent_txid1].utxos[0].address, taddr2); + assert_eq!(txs[&sent_txid1].utxos[0].spent, Some(sent_txid2)); + assert_eq!(txs[&sent_txid1].utxos[0].unconfirmed_spent, None); + } + + // Now, spend to an external z address, which will select all the utxos + let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32])); + let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(), + &fvk.default_address().unwrap().1); + + const AMOUNT_SENT_EXT: u64 = 45; + let outgoing_memo = "Outgoing Memo".to_string(); + + // Create a tx and send to address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_address, AMOUNT_SENT_EXT, Some(outgoing_memo.clone()))]).unwrap(); + + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid3 = sent_tx.txid(); + + let mut cb5 = FakeCompactBlock::new(4, cb4.hash()); + cb5.add_tx(&sent_tx); + wallet.scan_block(&cb5.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx, 4, 0); + + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&sent_txid3].outgoing_metadata.len(), 1); + + assert_eq!(txs[&sent_txid3].outgoing_metadata[0].address, ext_address); + assert_eq!(txs[&sent_txid3].outgoing_metadata[0].value, AMOUNT_SENT_EXT); + assert_eq!(txs[&sent_txid3].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); + + // Test to see that the UTXOs were spent. + + // UTXO2 + assert_eq!(txs[&sent_txid2].utxos[0].value, AMOUNT_SENT2); + assert_eq!(txs[&sent_txid2].utxos[0].address, taddr3); + assert_eq!(txs[&sent_txid2].utxos[0].spent, Some(sent_txid3)); + assert_eq!(txs[&sent_txid2].utxos[0].unconfirmed_spent, None); + } + +} + +#[test] +fn test_multi_spends() { + const AMOUNT1: u64 = 50000; + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); + + let zaddr2 = wallet.add_zaddr(); + const ZAMOUNT2:u64 = 30; + let outgoing_memo2 = "Outgoing Memo2".to_string(); + + let zaddr3 = wallet.add_zaddr(); + const ZAMOUNT3:u64 = 40; + let outgoing_memo3 = "Outgoing Memo3".to_string(); + + let taddr2 = wallet.add_taddr(); + const TAMOUNT2:u64 = 50; + let taddr3 = wallet.add_taddr(); + const TAMOUNT3:u64 = 60; + + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + let tos = vec![ (zaddr2.as_str(), ZAMOUNT2, Some(outgoing_memo2.clone())), + (zaddr3.as_str(), ZAMOUNT3, Some(outgoing_memo3.clone())), + (taddr2.as_str(), TAMOUNT2, None), + (taddr3.as_str(), TAMOUNT3, None) ]; + + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, tos).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid = sent_tx.txid(); + + let mut cb3 = FakeCompactBlock::new(2, block_hash); + cb3.add_tx(&sent_tx); + wallet.scan_block(&cb3.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx, 2, 0); + + // Make sure all the outputs are there! + { + let txs = wallet.txs.read().unwrap(); + + // The single note was spent + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + + // The outputs are all sent to the wallet, so they should + // correspond to notes & utxos. + // 2 notes + 1 change + assert_eq!(txs[&sent_txid].notes.len(), 3); + + // Find the change note + let change_note = txs[&sent_txid].notes.iter().find(|n| n.is_change).unwrap(); + assert_eq!(change_note.note.value, AMOUNT1 - (ZAMOUNT2+ZAMOUNT3+TAMOUNT2+TAMOUNT3+fee)); + assert_eq!(change_note.spent, None); + assert_eq!(change_note.unconfirmed_spent, None); + + // Find zaddr2 + let zaddr2_note = txs[&sent_txid].notes.iter().find(|n| n.note.value == ZAMOUNT2).unwrap(); + assert_eq!(zaddr2_note.account, 2-1); + assert_eq!(zaddr2_note.is_change, false); + assert_eq!(zaddr2_note.spent, None); + assert_eq!(zaddr2_note.unconfirmed_spent, None); + assert_eq!(LightWallet::memo_str(&zaddr2_note.memo), Some(outgoing_memo2)); + + // Find zaddr3 + let zaddr3_note = txs[&sent_txid].notes.iter().find(|n| n.note.value == ZAMOUNT3).unwrap(); + assert_eq!(zaddr3_note.account, 3-1); + assert_eq!(zaddr3_note.is_change, false); + assert_eq!(zaddr3_note.spent, None); + assert_eq!(zaddr3_note.unconfirmed_spent, None); + assert_eq!(LightWallet::memo_str(&zaddr3_note.memo), Some(outgoing_memo3)); + + // Find taddr2 + let utxo2 = txs[&sent_txid].utxos.iter().find(|u| u.value == TAMOUNT2).unwrap(); + assert_eq!(utxo2.address, taddr2); + assert_eq!(utxo2.txid, sent_txid); + assert_eq!(utxo2.spent, None); + assert_eq!(utxo2.unconfirmed_spent, None); + + // Find taddr3 + let utxo3 = txs[&sent_txid].utxos.iter().find(|u| u.value == TAMOUNT3).unwrap(); + assert_eq!(utxo3.address, taddr3); + assert_eq!(utxo3.txid, sent_txid); + assert_eq!(utxo3.spent, None); + assert_eq!(utxo3.unconfirmed_spent, None); + } + + // Now send an outgoing tx to one ext taddr and one ext zaddr + let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32])); + let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(), + &fvk.default_address().unwrap().1); + let ext_memo = "External memo".to_string(); + let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); + + const EXT_ZADDR_AMOUNT: u64 = 3000; + let ext_taddr_amount = AMOUNT1 - fee - EXT_ZADDR_AMOUNT - fee; // Spend everything + println!("taddr amount {}", ext_taddr_amount); + + let tos = vec![ (ext_address.as_str(), EXT_ZADDR_AMOUNT, Some(ext_memo.clone())), + (ext_taddr.as_str(), ext_taddr_amount, None)]; + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, tos).unwrap(); + let sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid2 = sent_tx.txid(); + + let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); + cb4.add_tx(&sent_tx); + wallet.scan_block(&cb4.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx, 3, 0); + + // Make sure all the outputs are there! + { + let txs = wallet.txs.read().unwrap(); + + // All notes were spent + assert_eq!(txs[&sent_txid].notes[0].spent, Some(sent_txid2)); + assert_eq!(txs[&sent_txid].notes[1].spent, Some(sent_txid2)); + assert_eq!(txs[&sent_txid].notes[2].spent, Some(sent_txid2)); + + // All utxos were spent + assert_eq!(txs[&sent_txid].utxos[0].spent, Some(sent_txid2)); + assert_eq!(txs[&sent_txid].utxos[1].spent, Some(sent_txid2)); + + // The new tx has no change + assert_eq!(txs[&sent_txid2].notes.len(), 0); + assert_eq!(txs[&sent_txid2].utxos.len(), 0); + + // Test the outgoing metadata + // Find the znote + let zoutgoing = txs[&sent_txid2].outgoing_metadata.iter().find(|o| o.address == ext_address).unwrap(); + assert_eq!(zoutgoing.value, EXT_ZADDR_AMOUNT); + assert_eq!(LightWallet::memo_str(&Some(zoutgoing.memo.clone())), Some(ext_memo)); + + // Find the taddr + let toutgoing = txs[&sent_txid2].outgoing_metadata.iter().find(|o| o.address == ext_taddr).unwrap(); + assert_eq!(toutgoing.value, ext_taddr_amount); + assert_eq!(LightWallet::memo_str(&Some(toutgoing.memo.clone())), None); + } +} + +#[test] +fn test_bad_send() { + // Test all the ways in which a send should fail + const AMOUNT1: u64 = 50000; + let _fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let (wallet, _txid1, _block_hash) = get_test_wallet(AMOUNT1); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); + + // Bad address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&"badaddress", 10, None)]); + assert!(raw_tx.err().unwrap().contains("Invalid recipient address")); + + // Insufficient funds + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_taddr, AMOUNT1 + 10, None)]); + assert!(raw_tx.err().unwrap().contains("Insufficient verified funds")); +} + +#[test] +#[should_panic] +fn test_bad_params() { + let (wallet, _, _) = get_test_wallet(100000); + let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + // Bad params + let _ = wallet.send_to_address(branch_id, &[], &[], + vec![(&ext_taddr, 10, None)]); +} + +/// Test helper to add blocks +fn add_blocks(wallet: &LightWallet, start: i32, num: i32, mut prev_hash: BlockHash) -> Result{ + // Add it to a block + let mut new_blk = FakeCompactBlock::new(start, prev_hash); + for i in 0..num { + new_blk = FakeCompactBlock::new(start+i, prev_hash); + prev_hash = new_blk.hash(); + match wallet.scan_block(&new_blk.as_bytes()) { + Ok(_) => {}, // continue + Err(e) => return Err(e) + }; + } + + Ok(new_blk.hash()) +} + +#[test] +fn test_block_limit() { + const AMOUNT: u64 = 500000; + let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT); + + let prev_hash = add_blocks(&wallet, 2, 1, block_hash).unwrap(); + assert_eq!(wallet.blocks.read().unwrap().len(), 3); + + let prev_hash = add_blocks(&wallet, 3, 47, prev_hash).unwrap(); + assert_eq!(wallet.blocks.read().unwrap().len(), 50); + + let prev_hash = add_blocks(&wallet, 50, 51, prev_hash).unwrap(); + assert_eq!(wallet.blocks.read().unwrap().len(), 101); + + // Subsequent blocks should start to trim + let prev_hash = add_blocks(&wallet, 101, 1, prev_hash).unwrap(); + assert_eq!(wallet.blocks.read().unwrap().len(), 101); + + // Add lots + let _ = add_blocks(&wallet, 102, 10, prev_hash).unwrap(); + assert_eq!(wallet.blocks.read().unwrap().len(), 101); + + // Now clear the blocks + wallet.clear_blocks(); + assert_eq!(wallet.blocks.read().unwrap().len(), 0); + + let prev_hash = add_blocks(&wallet, 0, 1, BlockHash([0;32])).unwrap(); + assert_eq!(wallet.blocks.read().unwrap().len(), 1); + + let _ = add_blocks(&wallet, 1, 10, prev_hash).unwrap(); + assert_eq!(wallet.blocks.read().unwrap().len(), 11); +} + +#[test] +fn test_rollback() { + const AMOUNT: u64 = 500000; + + let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT); + + add_blocks(&wallet, 2, 5, block_hash).unwrap(); + + // Make sure the note exists with the witnesses + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes[0].witnesses.len(), 7); + } + + // Invalidate 2 blocks + assert_eq!(wallet.last_scanned_height(), 6); + assert_eq!(wallet.invalidate_block(5), 2); + + // THe witnesses should be rolledback + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes[0].witnesses.len(), 5); + } + + let blk3_hash; + let blk4_hash; + { + let blks = wallet.blocks.read().unwrap(); + blk3_hash = blks[3].hash.clone(); + blk4_hash = blks[4].hash.clone(); + } + + // This should result in an exception, because the "prevhash" is wrong + assert!(add_blocks(&wallet, 5, 2, blk3_hash).is_err(), + "Shouldn't be able to add because of invalid prev hash"); + + // Add with the proper prev hash + add_blocks(&wallet, 5, 2, blk4_hash).unwrap(); + + let blk6_hash; + { + let blks = wallet.blocks.read().unwrap(); + blk6_hash = blks[6].hash.clone(); + } + + // Now do a Tx + let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).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 + 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 sent_tx = Transaction::read(&raw_tx[..]).unwrap(); + let sent_txid = sent_tx.txid(); + let mut cb3 = FakeCompactBlock::new(7, blk6_hash); + cb3.add_tx(&sent_tx); + wallet.scan_block(&cb3.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx, 7, 0); + + // Make sure the Tx is in. + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + + // The sent tx should generate change + assert_eq!(txs[&sent_txid].notes.len(), 1); + assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT - AMOUNT_SENT - fee); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, None); + assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); + assert_eq!(txs[&sent_txid].notes[0].witnesses.len(), 1); + } + + // Invalidate 3 blocks + assert_eq!(wallet.last_scanned_height(), 7); + assert_eq!(wallet.invalidate_block(5), 3); + assert_eq!(wallet.last_scanned_height(), 4); + + // Make sure the orig Tx is there, but new Tx has disappeared + { + let txs = wallet.txs.read().unwrap(); + + // Orig Tx is still there, since this is in block 0 + // But now the spent tx is gone + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT); + assert_eq!(txs[&txid1].notes[0].spent, None); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + + // The sent tx is missing + assert!(txs.get(&sent_txid).is_none()); + } +} + +#[test] +fn test_t_derivation() { + let lc = 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: 1, + no_cert_verification: false, + }; + + let seed_phrase = Some("chimney better bulb horror rebuild whisper improve intact letter giraffe brave rib appear bulk aim burst snap salt hill sad merge tennis phrase raise".to_string()); + + let wallet = LightWallet::new(seed_phrase.clone(), &lc, 0).unwrap(); + + // Test the addresses against https://iancoleman.io/bip39/ + let (taddr, pk) = &wallet.get_t_secret_keys()[0]; + assert_eq!(taddr, "t1eQ63fwkQ4n4Eo5uCrPGaAV8FWB2tmx7ui"); + assert_eq!(pk, "Kz9ybX4giKag4NtnP1pi8WQF2B2hZDkFU85S7Dciz3UUhM59AnhE"); + + // Test a couple more + wallet.add_taddr(); + let (taddr, pk) = &wallet.get_t_secret_keys()[1]; + assert_eq!(taddr, "t1NoS6ZgaUTpmjkge2cVpXGcySasdYDrXqh"); + assert_eq!(pk, "KxdmS38pxskS6bbKX43zhTu8ppWckNmWjKsQFX1hwidvhRRgRd3c"); + + let (zaddr, sk) = &wallet.get_z_private_keys()[0]; + assert_eq!(zaddr, "zs1q6xk3q783t5k92kjqt2rkuuww8pdw2euzy5rk6jytw97enx8fhpazdv3th4xe7vsk6e9sfpawfg"); + assert_eq!(sk, "secret-extended-key-main1qvpa0qr8qqqqpqxn4l054nzxpxzp3a8r2djc7sekdek5upce8mc2j2z0arzps4zv940qeg706hd0wq6g5snzvhp332y6vhwyukdn8dhekmmsk7fzvzkqm6ypc99uy63tpesqwxhpre78v06cx8k5xpp9mrhtgqs5dvp68cqx2yrvthflmm2ynl8c0506dekul0f6jkcdmh0292lpphrksyc5z3pxwws97zd5els3l2mjt2s7hntap27mlmt6w0drtfmz36vz8pgu7ec0twfrq"); + + assert_eq!(seed_phrase, Some(wallet.get_seed_phrase())); +} + +#[test] +fn test_lock_unlock() { + const AMOUNT: u64 = 500000; + + let (mut wallet, _, _) = get_test_wallet(AMOUNT); + let config = wallet.config.clone(); + + // Add some addresses + let zaddr0 = encode_payment_address(config.hrp_sapling_address(), + &wallet.extfvks.read().unwrap()[0].default_address().unwrap().1); + let zaddr1 = wallet.add_zaddr(); + let zaddr2 = wallet.add_zaddr(); + + let taddr0 = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]); + let taddr1 = wallet.add_taddr(); + let taddr2 = wallet.add_taddr(); + + let seed = wallet.seed; + + // Trying to lock a wallet that's not encrpyted is an error + assert!(wallet.lock().is_err()); + + // Encrypt the wallet + wallet.encrypt("somepassword".to_string()).unwrap(); + + // Encrypting an already encrypted wallet should fail + assert!(wallet.encrypt("somepassword".to_string()).is_err()); + + // Serialize a locked wallet + let mut serialized_data = vec![]; + wallet.write(&mut serialized_data).expect("Serialize wallet"); + + // Should fail when there's a wrong password + assert!(wallet.unlock("differentpassword".to_string()).is_err()); + + // Properly unlock + wallet.unlock("somepassword".to_string()).unwrap(); + + assert_eq!(seed, wallet.seed); + { + let extsks = wallet.extsks.read().unwrap(); + let tkeys = wallet.tkeys.read().unwrap(); + assert_eq!(extsks.len(), 3); + assert_eq!(tkeys.len(), 3); + + assert_eq!(zaddr0, encode_payment_address(config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[0]).default_address().unwrap().1)); + assert_eq!(zaddr1, encode_payment_address(config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[1]).default_address().unwrap().1)); + assert_eq!(zaddr2, encode_payment_address(config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[2]).default_address().unwrap().1)); + + assert_eq!(taddr0, wallet.address_from_sk(&tkeys[0])); + assert_eq!(taddr1, wallet.address_from_sk(&tkeys[1])); + assert_eq!(taddr2, wallet.address_from_sk(&tkeys[2])); + } + + // Unlocking an already unlocked wallet should fail + assert!(wallet.unlock("somepassword".to_string()).is_err()); + + // Trying to serialize a encrypted but unlocked wallet should fail + assert!(wallet.write(&mut vec![]).is_err()); + + // ...but if we lock it again, it should serialize + wallet.lock().unwrap(); + wallet.write(&mut vec![]).expect("Serialize wallet"); + + // Locking an already locked wallet is an error + assert!(wallet.lock().is_err()); + + // Try from a deserialized, locked wallet + let mut wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap(); + wallet2.unlock("somepassword".to_string()).unwrap(); + + assert_eq!(seed, wallet2.seed); + { + let extsks = wallet2.extsks.read().unwrap(); + let tkeys = wallet2.tkeys.read().unwrap(); + assert_eq!(extsks.len(), 3); + assert_eq!(tkeys.len(), 3); + + assert_eq!(zaddr0, encode_payment_address(wallet2.config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[0]).default_address().unwrap().1)); + assert_eq!(zaddr1, encode_payment_address(wallet2.config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[1]).default_address().unwrap().1)); + assert_eq!(zaddr2, encode_payment_address(wallet2.config.hrp_sapling_address(), + &ExtendedFullViewingKey::from(&extsks[2]).default_address().unwrap().1)); + + assert_eq!(taddr0, wallet2.address_from_sk(&tkeys[0])); + assert_eq!(taddr1, wallet2.address_from_sk(&tkeys[1])); + assert_eq!(taddr2, wallet2.address_from_sk(&tkeys[2])); + } +} + +#[test] +#[should_panic] +fn test_invalid_bip39_t() { + // Passing a 32-byte seed to bip32 should fail. + let config = get_test_config(); + LightWallet::get_taddr_from_bip39seed(&config, &[0u8; 32], 0); +} + +#[test] +#[should_panic] +fn test_invalid_bip39_z() { + // Passing a 32-byte seed to bip32 should fail. + let config = get_test_config(); + LightWallet::get_zaddr_from_bip39seed(&config, &[0u8; 32], 0); +} + +#[test] +fn test_invalid_scan_blocks() { + const AMOUNT: u64 = 500000; + let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT); + + let prev_hash = add_blocks(&wallet, 2, 1, block_hash).unwrap(); + assert_eq!(wallet.blocks.read().unwrap().len(), 3); + + // Block fails to scan for bad encoding + assert_eq!(wallet.scan_block(&[0; 32]), Err(-1)); + + // Block is invalid height + let new_blk = FakeCompactBlock::new(4, prev_hash); + assert_eq!(wallet.scan_block(&new_blk.as_bytes()), Err(2)); + + // Block is right height, but invalid prev height (for reorgs) + let new_blk = FakeCompactBlock::new(2, BlockHash([0; 32])); + assert_eq!(wallet.scan_block(&new_blk.as_bytes()), Err(2)); + + // Block is right height, but invalid prev height (for reorgs) + let new_blk = FakeCompactBlock::new(3, BlockHash([0; 32])); + assert_eq!(wallet.scan_block(&new_blk.as_bytes()), Err(2)); + + // Then the rest add properly + let _ = add_blocks(&wallet, 3, 2, prev_hash).unwrap(); + assert_eq!(wallet.blocks.read().unwrap().len(), 5); +} + +#[test] +fn test_encrypted_zreceive() { + const AMOUNT1: u64 = 50000; + let password: String = "password".to_string(); + + let (mut wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); + + 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 outgoing_memo = "Outgoing Memo".to_string(); + let fee: u64 = DEFAULT_FEE.try_into().unwrap(); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + // Create a tx and send to address + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))]).unwrap(); + + // Now that we have the transaction, we'll encrypt the wallet + wallet.encrypt(password.clone()).unwrap(); + + // Scan the tx and make sure it gets added + 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(); + + // Now, full scan the Tx, which should populate the Outgoing Meta data + wallet.scan_full_tx(&sent_tx, 2, 0); + + // Now this new Spent tx should be in, so the note should be marked confirmed spent + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + + // The sent tx should generate change + assert_eq!(txs[&sent_txid].notes.len(), 1); + assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, None); + assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); + + // Outgoing Metadata + assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); + + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); + + assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, ext_address); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo); + } + + // 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()); + + // unlock the wallet so we can spend to the second z address + wallet.unlock(password.clone()).unwrap(); + + // Second z address + let zaddr2 = wallet.add_zaddr(); + 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(); + + // Now lock the wallet again + wallet.lock().unwrap(); + + let sent_tx2 = Transaction::read(&raw_tx[..]).unwrap(); + let txid2 = sent_tx2.txid(); + + let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); + cb4.add_tx(&sent_tx2); + wallet.scan_block(&cb4.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx2, 3, 0); + + { + let txs = wallet.txs.read().unwrap(); + let prev_change_value = AMOUNT1 - AMOUNT_SENT - fee; + + // Change note from prev transaction is spent + assert_eq!(txs[&sent_txid].notes[0].note.value, prev_change_value); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, Some(txid2)); + + // New change note. So find it. + let change_note = txs[&txid2].notes.iter().find(|n| n.is_change).unwrap(); + + // New incoming tx is present + assert_eq!(change_note.note.value, prev_change_value - (ZAMOUNT2+fee)); + assert_eq!(change_note.spent, None); + assert_eq!(change_note.unconfirmed_spent, None); + + // Find zaddr2 + let zaddr2_note = txs[&txid2].notes.iter().find(|n| n.note.value == ZAMOUNT2).unwrap(); + assert_eq!(zaddr2_note.account, 1); + assert_eq!(zaddr2_note.is_change, false); + assert_eq!(zaddr2_note.spent, None); + assert_eq!(zaddr2_note.unconfirmed_spent, None); + assert_eq!(LightWallet::memo_str(&zaddr2_note.memo), Some(outgoing_memo2)); + } +} + + +#[test] +fn test_encrypted_treceive() { + const AMOUNT1: u64 = 50000; + let password: String = "password".to_string(); + let (mut wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); + + let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap(); + let (ss, so) = get_sapling_params().unwrap(); + + let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap()); + 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(); + + // Now that we have the transaction, we'll encrypt the wallet + wallet.encrypt(password.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); + + // Now this new Spent tx should be in, so the note should be marked confirmed spent + { + let txs = wallet.txs.read().unwrap(); + assert_eq!(txs[&txid1].notes.len(), 1); + assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1); + assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid)); + assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None); + + // The sent tx should generate change + assert_eq!(txs[&sent_txid].notes.len(), 1); + assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, None); + assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None); + + // Outgoing Metadata + assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, taddr); + assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT); + assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1); + } + + // 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()); + + // unlock the wallet so we can spend to the second z address + wallet.unlock(password.clone()).unwrap(); + + // Second z address + 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(); + + // Now lock the wallet again + wallet.lock().unwrap(); + + let sent_tx2 = Transaction::read(&raw_tx[..]).unwrap(); + let txid2 = sent_tx2.txid(); + + let mut cb4 = FakeCompactBlock::new(3, cb3.hash()); + cb4.add_tx(&sent_tx2); + wallet.scan_block(&cb4.as_bytes()).unwrap(); + wallet.scan_full_tx(&sent_tx2, 3, 0); + + { + let txs = wallet.txs.read().unwrap(); + let prev_change_value = AMOUNT1 - AMOUNT_SENT - fee; + + // Change note from prev transaction is spent + assert_eq!(txs[&sent_txid].notes[0].note.value, prev_change_value); + assert_eq!(txs[&sent_txid].notes[0].is_change, true); + assert_eq!(txs[&sent_txid].notes[0].spent, Some(txid2)); + + // New change note. So find it. + let change_note = txs[&txid2].notes.iter().find(|n| n.is_change).unwrap(); + + // New incoming tx is present + assert_eq!(change_note.note.value, prev_change_value - (TAMOUNT2+fee)); + assert_eq!(change_note.spent, None); + assert_eq!(change_note.unconfirmed_spent, None); + + // Find taddr2 + let utxo2 = txs[&txid2].utxos.iter().find(|u| u.value == TAMOUNT2).unwrap(); + assert_eq!(txs[&txid2].utxos.len(), 1); + assert_eq!(utxo2.address, taddr2); + assert_eq!(utxo2.txid, txid2); + assert_eq!(utxo2.spent, None); + assert_eq!(utxo2.unconfirmed_spent, None); + } +} From 51b1b54863c210f3e6f6950cccd5cb0d5256254b Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Mon, 21 Oct 2019 17:56:28 -0700 Subject: [PATCH 3/7] Add wallet creation tests --- lib/Cargo.toml | 3 ++ lib/src/lightclient.rs | 90 ++++++++++++++++++++++++++++-------- lib/src/lightwallet/tests.rs | 2 + 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 748da13..45ed1d8 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -70,5 +70,8 @@ features = ["ff_derive"] [build-dependencies] tower-grpc-build = { git = "https://github.com/tower-rs/tower-grpc", features = ["tower-hyper"] } +[dev-dependencies] +tempdir = "0.3.7" + [profile.release] debug = false \ No newline at end of file diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 0373a7e..112c607 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -5,7 +5,7 @@ use rand::{rngs::OsRng, seq::SliceRandom}; use std::sync::{Arc, RwLock}; use std::sync::atomic::{AtomicU64, AtomicI32, AtomicUsize, Ordering}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::fs::File; use std::collections::HashMap; use std::io; @@ -38,12 +38,13 @@ pub struct LightClientConfig { pub consensus_branch_id : String, pub anchor_offset : u32, pub no_cert_verification : bool, + pub data_dir : Option } impl LightClientConfig { // Create an unconnected (to any server) config to test for local wallet etc... - pub fn create_unconnected(chain_name: String) -> LightClientConfig { + pub fn create_unconnected(chain_name: String, dir: Option) -> LightClientConfig { LightClientConfig { server : http::Uri::default(), chain_name : chain_name, @@ -51,6 +52,7 @@ impl LightClientConfig { consensus_branch_id : "".to_string(), anchor_offset : ANCHOR_OFFSET, no_cert_verification : false, + data_dir : dir, } } @@ -67,6 +69,7 @@ impl LightClientConfig { consensus_branch_id : info.consensus_branch_id, anchor_offset : ANCHOR_OFFSET, no_cert_verification : dangerous, + data_dir : None, }; Ok((config, info.block_height)) @@ -74,20 +77,24 @@ impl LightClientConfig { pub fn get_zcash_data_path(&self) -> Box { let mut zcash_data_location; - if cfg!(target_os="macos") || cfg!(target_os="windows") { - zcash_data_location = dirs::data_dir().expect("Couldn't determine app data directory!"); - zcash_data_location.push("Zcash"); + if self.data_dir.is_some() { + zcash_data_location = PathBuf::from(&self.data_dir.as_ref().unwrap()); } else { - zcash_data_location = dirs::home_dir().expect("Couldn't determine home directory!"); - zcash_data_location.push(".zcash"); - }; + if cfg!(target_os="macos") || cfg!(target_os="windows") { + zcash_data_location = dirs::data_dir().expect("Couldn't determine app data directory!"); + zcash_data_location.push("Zcash"); + } else { + zcash_data_location = dirs::home_dir().expect("Couldn't determine home directory!"); + zcash_data_location.push(".zcash"); + }; - match &self.chain_name[..] { - "main" => {}, - "test" => zcash_data_location.push("testnet3"), - "regtest" => zcash_data_location.push("regtest"), - c => panic!("Unknown chain {}", c), - }; + match &self.chain_name[..] { + "main" => {}, + "test" => zcash_data_location.push("testnet3"), + "regtest" => zcash_data_location.push("regtest"), + c => panic!("Unknown chain {}", c), + }; + } zcash_data_location.into_boxed_path() } @@ -226,8 +233,8 @@ impl LightClient { /// Method to create a test-only version of the LightClient #[allow(dead_code)] - fn unconnected(seed_phrase: String) -> io::Result { - let config = LightClientConfig::create_unconnected("test".to_string()); + fn unconnected(seed_phrase: String, dir: Option) -> io::Result { + let config = LightClientConfig::create_unconnected("test".to_string(), dir); let mut l = LightClient { wallet : Arc::new(RwLock::new(LightWallet::new(Some(seed_phrase), &config, 0)?)), config : config.clone(), @@ -914,7 +921,8 @@ impl LightClient { #[cfg(test)] pub mod tests { use lazy_static::lazy_static; - //use super::LightClient; + use tempdir::TempDir; + use super::{LightClient, LightClientConfig}; lazy_static!{ static ref TEST_SEED: String = "youth strong sweet gorilla hammer unhappy congress stamp left stereo riot salute road tag clean toilet artefact fork certain leopard entire civil degree wonder".to_string(); @@ -922,7 +930,7 @@ pub mod tests { #[test] pub fn test_encrypt_decrypt() { - let lc = super::LightClient::unconnected(TEST_SEED.to_string()).unwrap(); + let lc = super::LightClient::unconnected(TEST_SEED.to_string(), None).unwrap(); assert!(!lc.do_export(None).is_err()); assert!(!lc.do_new_address("z").is_err()); @@ -947,7 +955,7 @@ pub mod tests { #[test] pub fn test_addresses() { - let lc = super::LightClient::unconnected(TEST_SEED.to_string()).unwrap(); + let lc = super::LightClient::unconnected(TEST_SEED.to_string(), None).unwrap(); // Add new z and t addresses @@ -966,5 +974,49 @@ pub mod tests { assert_eq!(addresses["t_addresses"][2], taddr2); } + #[test] + pub fn test_wallet_creation() { + // Create a new tmp director + { + let tmp = TempDir::new("lctest").unwrap(); + let dir_name = tmp.path().to_str().map(|s| s.to_string()); + + // A lightclient to a new, empty directory works. + let config = LightClientConfig::create_unconnected("test".to_string(), dir_name); + let lc = LightClient::new(&config, 0).unwrap(); + let seed = lc.do_seed_phrase().unwrap()["seed"].as_str().unwrap().to_string(); + lc.do_save().unwrap(); + + // Doing another new will fail, because the wallet file now already exists + assert!(LightClient::new(&config, 0).is_err()); + + // new_from_phrase will not work either, again, because wallet file exists + assert!(LightClient::new_from_phrase(TEST_SEED.to_string(), &config, 0).is_err()); + + // Creating a lightclient to the same dir without a seed should re-read the same wallet + // file and therefore the same seed phrase + let lc2 = LightClient::read_from_disk(&config).unwrap(); + assert_eq!(seed, lc2.do_seed_phrase().unwrap()["seed"].as_str().unwrap().to_string()); + } + + // Now, get a new directory, and try to read from phrase + { + let tmp = TempDir::new("lctest").unwrap(); + let dir_name = tmp.path().to_str().map(|s| s.to_string()); + + let config = LightClientConfig::create_unconnected("test".to_string(), dir_name); + + // read_from_disk will fail, because the dir doesn't exist + assert!(LightClient::read_from_disk(&config).is_err()); + + // New from phrase should work becase a file doesn't exist already + let lc = LightClient::new_from_phrase(TEST_SEED.to_string(), &config, 0).unwrap(); + assert_eq!(TEST_SEED.to_string(), lc.do_seed_phrase().unwrap()["seed"].as_str().unwrap().to_string()); + lc.do_save().unwrap(); + + // Now a new will fail because wallet exists + assert!(LightClient::new(&config, 0).is_err()); + } + } } \ No newline at end of file diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index 3199c2d..bda0847 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -657,6 +657,7 @@ fn get_test_config() -> LightClientConfig { consensus_branch_id: "000000".to_string(), anchor_offset: 0, no_cert_verification: false, + data_dir: None, } } @@ -1580,6 +1581,7 @@ fn test_t_derivation() { consensus_branch_id: "000000".to_string(), anchor_offset: 1, no_cert_verification: false, + data_dir: None, }; let seed_phrase = Some("chimney better bulb horror rebuild whisper improve intact letter giraffe brave rib appear bulk aim burst snap salt hill sad merge tennis phrase raise".to_string()); From c014dbc4d90c3dec65b501923e7bf6855c8e80d5 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Mon, 21 Oct 2019 18:00:19 -0700 Subject: [PATCH 4/7] Add parse comment --- lib/src/lightclient.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 112c607..1e570f0 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -771,6 +771,8 @@ impl LightClient { return; } + // Parse the block and save it's time. We'll use this timestamp for + // transactions in this block that might belong to us. let block: Result = parse_from_bytes(encoded_block); match block { From 581d882a5f23a82b5311d622a2a9f507abe54c14 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Mon, 21 Oct 2019 20:43:30 -0700 Subject: [PATCH 5/7] Additional tests --- lib/src/lightwallet.rs | 17 ++++++++++++++--- lib/src/lightwallet/tests.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 74f14dd..7a37c1b 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -1313,11 +1313,22 @@ impl LightWallet { } let start_time = now(); + if tos.len() == 0 { + return Err("Need at least one destination address".to_string()); + } - let total_value = tos.iter().map(|to| to.1).sum::(); - - // TODO: Check for duplicates in destination addresses + // Check for duplicates in the to list + if tos.len() > 1 { + let mut to_addresses = tos.iter().map(|t| t.0.to_string()).collect::>(); + to_addresses.sort(); + for i in 0..to_addresses.len()-1 { + if to_addresses[i] == to_addresses[i+1] { + return Err(format!("To address {} is duplicated", to_addresses[i])); + } + } + } + let total_value = tos.iter().map(|to| to.1).sum::(); println!( "0: Creating transaction sending {} ztoshis to {} addresses", total_value, tos.len() diff --git a/lib/src/lightwallet/tests.rs b/lib/src/lightwallet/tests.rs index bda0847..469d022 100644 --- a/lib/src/lightwallet/tests.rs +++ b/lib/src/lightwallet/tests.rs @@ -1407,6 +1407,17 @@ fn test_bad_send() { let raw_tx = wallet.send_to_address(branch_id, &ss, &so, vec![(&ext_taddr, AMOUNT1 + 10, None)]); assert!(raw_tx.err().unwrap().contains("Insufficient verified funds")); + + // Duplicated addresses + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, + vec![(&ext_taddr, AMOUNT1 + 10, None), + (&ext_taddr, AMOUNT1 + 10, None)]); + assert!(raw_tx.err().unwrap().contains("duplicate")); + + // No addresses + let raw_tx = wallet.send_to_address(branch_id, &ss, &so, vec![]); + assert!(raw_tx.err().unwrap().contains("at least one")); + } #[test] @@ -1698,6 +1709,30 @@ fn test_lock_unlock() { assert_eq!(taddr1, wallet2.address_from_sk(&tkeys[1])); assert_eq!(taddr2, wallet2.address_from_sk(&tkeys[2])); } + + // Remove encryption from a unlocked wallet should succeed + wallet2.remove_encryption("somepassword".to_string()).unwrap(); + assert_eq!(seed, wallet2.seed); + + // Now encrypt with a different password + wallet2.encrypt("newpassword".to_string()).unwrap(); + assert_eq!([0u8; 32], wallet2.seed); // Seed is cleared out + + // Locking should fail because it is already locked + assert!(wallet2.lock().is_err()); + + // The old password shouldn't work + assert!(wallet2.remove_encryption("somepassword".to_string()).is_err()); + + // Remove encryption with the right password + wallet2.remove_encryption("newpassword".to_string()).unwrap(); + assert_eq!(seed, wallet2.seed); + + // Unlocking a wallet without encryption is an error + assert!(wallet2.remove_encryption("newpassword".to_string()).is_err()); + // Can't lock/unlock a wallet that's not encrypted + assert!(wallet2.lock().is_err()); + assert!(wallet2.unlock("newpassword".to_string()).is_err()); } #[test] From 9324f7f097ca0df6fb42c6b56f4a0a7727f457c4 Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Tue, 22 Oct 2019 07:42:43 -0700 Subject: [PATCH 6/7] Pin tokio dependencies --- docker/Dockerfile | 8 +++++++- lib/Cargo.toml | 6 +++--- lib/src/lightwallet.rs | 6 +++--- src/main.rs | 1 + 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 9c78c80..abe4b05 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -28,4 +28,10 @@ RUN cp /usr/x86_64-w64-mingw32/lib/crt2.o /usr/local/rustup/toolchains/1.38.0-x8 # For windows cross compilation, use a pre-build binary. Remember to set the # SODIUM_LIB_DIR for windows cross compilation RUN cd /opt && wget https://download.libsodium.org/libsodium/releases/libsodium-1.0.17-mingw.tar.gz && \ - tar xvf libsodium-1.0.17-mingw.tar.gz \ No newline at end of file + tar xvf libsodium-1.0.17-mingw.tar.gz + +# Cargo fetch the dependencies so we don't download them over and over again +RUN cd /tmp && git clone https://github.com/adityapk00/zecwallet-light-cli.git && \ + cd zecwallet-light-cli && \ + cargo fetch && \ + cd /tmp && rm -rf zecwallet-light-cli diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 45ed1d8..459ff9e 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2018" [dependencies] -tower-grpc = { git = "https://github.com/tower-rs/tower-grpc" } +tower-grpc = "0.1.1" futures = "0.1" bytes = "0.4" base58 = "0.1.0" @@ -14,7 +14,7 @@ dirs = "2.0.2" http = "0.1" prost = "0.5" tokio = "0.1" -tower-request-modifier = { git = "https://github.com/tower-rs/tower-http" } +tower-request-modifier = "0.1.0" tower-util = "0.1" hex = "0.3" protobuf = "2" @@ -31,7 +31,7 @@ tokio-rustls = "0.10.0-alpha.3" rustls = { version = "0.15.2", features = ["dangerous_configuration"] } webpki = "0.19.1" webpki-roots = "0.16.0" -tower-h2 = { git = "https://github.com/tower-rs/tower-h2" } +tower-h2 = { git = "https://github.com/tower-rs/tower-h2", rev="0865040d699697bbaf1c3b77b3f256b72f98cdf4" } rust-embed = { version = "5.1.0", features = ["debug-embed"] } rand = "0.7.2" sodiumoxide = "0.2.5" diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 7a37c1b..6cf8c2b 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -1399,9 +1399,9 @@ impl LightWallet { // Create a map from address -> sk for all taddrs, so we can spend from the // right address - let address_to_sk: HashMap<_, _> = self.tkeys.read().unwrap().iter().map(|sk| - (self.address_from_sk(&sk), sk.clone()) - ).collect(); + let address_to_sk = self.tkeys.read().unwrap().iter() + .map(|sk| (self.address_from_sk(&sk), sk.clone())) + .collect::>(); // Add all tinputs tinputs.iter() diff --git a/src/main.rs b/src/main.rs index 125f20a..3222923 100644 --- a/src/main.rs +++ b/src/main.rs @@ -317,6 +317,7 @@ fn attempt_recover_seed() { consensus_branch_id: "000000".to_string(), anchor_offset: 0, no_cert_verification: false, + data_dir: None, }; let mut reader = BufReader::new(File::open(config.get_wallet_path()).unwrap()); From bea9a26e3dcf6ed1dcc703848a942d343e38360c Mon Sep 17 00:00:00 2001 From: Aditya Kulkarni Date: Tue, 22 Oct 2019 08:08:48 -0700 Subject: [PATCH 7/7] Recover seed properly --- lib/src/lightclient.rs | 64 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 53 ++++++++++++---------------------- 2 files changed, 82 insertions(+), 35 deletions(-) diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs index 1e570f0..0d17f8a 100644 --- a/lib/src/lightclient.rs +++ b/lib/src/lightclient.rs @@ -327,6 +327,47 @@ impl LightClient { Ok(lc) } + pub fn attempt_recover_seed(config: &LightClientConfig) -> Result { + use std::io::prelude::*; + use byteorder::{LittleEndian, ReadBytesExt,}; + use bip39::{Mnemonic, Language}; + use zcash_primitives::serialize::Vector; + + let mut reader = BufReader::new(File::open(config.get_wallet_path()).unwrap()); + let version = reader.read_u64::().unwrap(); + println!("Reading wallet version {}", version); + + let encrypted = if version >= 4 { + reader.read_u8().unwrap() > 0 + } else { + false + }; + + if encrypted { + return Err("The wallet is encrypted!".to_string()); + } + + let mut enc_seed = [0u8; 48]; + if version >= 4 { + reader.read_exact(&mut enc_seed).unwrap(); + } + + let _nonce = if version >= 4 { + Vector::read(&mut reader, |r| r.read_u8()).unwrap() + } else { + vec![] + }; + + // Seed + let mut seed_bytes = [0u8; 32]; + reader.read_exact(&mut seed_bytes).unwrap(); + + let phrase = Mnemonic::from_entropy(&seed_bytes, Language::English,).unwrap().phrase().to_string(); + + Ok(phrase) + } + + pub fn last_scanned_height(&self) -> u64 { self.wallet.read().unwrap().last_scanned_height() as u64 } @@ -1021,4 +1062,27 @@ pub mod tests { } } + #[test] + pub fn test_recover_seed() { + // Create a new tmp director + { + let tmp = TempDir::new("lctest").unwrap(); + let dir_name = tmp.path().to_str().map(|s| s.to_string()); + + // A lightclient to a new, empty directory works. + let config = LightClientConfig::create_unconnected("test".to_string(), dir_name); + let lc = LightClient::new(&config, 0).unwrap(); + let seed = lc.do_seed_phrase().unwrap()["seed"].as_str().unwrap().to_string(); + lc.do_save().unwrap(); + + assert_eq!(seed, LightClient::attempt_recover_seed(&config).unwrap()); + + // Now encrypt and save the file + lc.wallet.write().unwrap().encrypt("password".to_string()).unwrap(); + lc.do_save().unwrap(); + + assert!(LightClient::attempt_recover_seed(&config).is_err()); + } + } + } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 3222923..a9f96ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::io::{Result, Error, ErrorKind}; +use std::io::{self, Error, ErrorKind}; use std::sync::Arc; use std::sync::mpsc::{channel, Sender, Receiver}; @@ -20,7 +20,7 @@ use log4rs::append::rolling_file::policy::compound::{ /// Build the Logging config -fn get_log_config(config: &LightClientConfig) -> Result { +fn get_log_config(config: &LightClientConfig) -> io::Result { let window_size = 3; // log0, log1, log2 let fixed_window_roller = FixedWindowRoller::builder().build("zecwallet-light-wallet-log{}",window_size).unwrap(); @@ -91,7 +91,21 @@ pub fn main() { .get_matches(); if matches.is_present("recover") { - attempt_recover_seed(); + // Create a Light Client Config in an attempt to recover the file. + let config = 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, + no_cert_verification: false, + data_dir: None, + }; + + match LightClient::attempt_recover_seed(&config) { + Ok(seed) => println!("Recovered seed: '{}'", seed), + Err(e) => eprintln!("Failed to recover seed. Error: {}", e) + }; return; } @@ -150,7 +164,7 @@ pub fn main() { } fn startup(server: http::Uri, dangerous: bool, seed: Option, first_sync: bool, print_updates: bool) - -> Result<(Sender<(String, Vec)>, Receiver)> { + -> io::Result<(Sender<(String, Vec)>, Receiver)> { // Try to get the configuration let (config, latest_block_height) = LightClientConfig::create(server.clone(), dangerous)?; @@ -301,34 +315,3 @@ fn command_loop(lightclient: Arc) -> (Sender<(String, Vec)> (command_tx, resp_rx) } - -fn attempt_recover_seed() { - use std::fs::File; - use std::io::prelude::*; - use std::io::{BufReader}; - use byteorder::{LittleEndian, ReadBytesExt,}; - use bip39::{Mnemonic, Language}; - - // Create a Light Client Config in an attempt to recover the file. - let config = 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, - no_cert_verification: false, - data_dir: None, - }; - - let mut reader = BufReader::new(File::open(config.get_wallet_path()).unwrap()); - let version = reader.read_u64::().unwrap(); - println!("Reading wallet version {}", version); - - // Seed - let mut seed_bytes = [0u8; 32]; - reader.read_exact(&mut seed_bytes).unwrap(); - - let phrase = Mnemonic::from_entropy(&seed_bytes, Language::English,).unwrap().phrase().to_string(); - - println!("Recovered seed phrase:\n{}", phrase); -}