// Copyright The Hush Developers 2019-2022 // Released under the GPLv3 use std::time::{SystemTime, Duration}; use std::io::{self, Read, Write}; use std::cmp; use std::collections::{HashMap, HashSet}; use std::sync::{Arc, RwLock}; use std::io::{Error, ErrorKind}; use threadpool::ThreadPool; use std::sync::mpsc::{channel}; use rand::{Rng, rngs::OsRng}; use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption}; use log::{info, warn, error}; use protobuf::parse_from_bytes; use libflate::gzip::{Decoder}; use secp256k1::SecretKey; use bip39::{Mnemonic, Language}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use pairing::bls12_381::{Bls12}; use sha2::{Sha256, Digest}; use sodiumoxide::crypto::secretbox; use zcash_client_backend::{ encoding::{encode_payment_address, encode_extended_spending_key, encode_extended_full_viewing_key, decode_extended_spending_key, decode_extended_full_viewing_key}, proto::compact_formats::{CompactBlock, CompactOutput}, wallet::{WalletShieldedOutput, WalletShieldedSpend} }; use zcash_primitives::{ jubjub::fs::Fs, block::BlockHash, serialize::{Vector}, transaction::{ builder::{Builder}, components::{Amount, OutPoint, TxOut}, components::amount::DEFAULT_FEE, TxId, Transaction, }, sapling::Node, merkle_tree::{CommitmentTree, IncrementalWitness}, legacy::{Script, TransparentAddress}, note_encryption::{Memo, try_sapling_note_decryption, try_sapling_output_recovery, try_sapling_compact_note_decryption}, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey, ChildIndex}, JUBJUB, primitives::{PaymentAddress}, }; use crate::lightclient::{LightClientConfig}; mod data; mod extended_key; mod utils; mod address; mod prover; mod walletzkey; use data::{BlockData, WalletTx, Utxo, SaplingNoteData, SpendableNote, OutgoingTxMetadata}; use extended_key::{KeyIndex, ExtendedPrivKey}; use walletzkey::{WalletZKey, WalletTKey, WalletZKeyType}; pub const MAX_REORG: usize = 100; pub const GAP_RULE_UNUSED_ADDRESSES: usize = 5; fn now() -> f64 { SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as f64 } /// Sha256(Sha256(value)) pub fn double_sha256(payload: &[u8]) -> Vec { let h1 = Sha256::digest(&payload); let h2 = Sha256::digest(&h1); h2.to_vec() } use base58::{ToBase58}; /// A trait for converting a [u8] to base58 encoded string. pub trait ToBase58Check { /// Converts a value of `self` to a base58 value, returning the owned string. /// The version is a coin-specific prefix that is added. /// The suffix is any bytes that we want to add at the end (like the "iscompressed" flag for /// Secret key encoding) fn to_base58check(&self, version: &[u8], suffix: &[u8]) -> String; } impl ToBase58Check for [u8] { fn to_base58check(&self, version: &[u8], suffix: &[u8]) -> String { let mut payload: Vec = Vec::new(); payload.extend_from_slice(version); payload.extend_from_slice(self); payload.extend_from_slice(suffix); let checksum = double_sha256(&payload); payload.append(&mut checksum[..4].to_vec()); payload.to_base58() } } pub struct LightWallet { // Is the wallet encrypted? If it is, then when writing to disk, the seed is always encrypted // and the individual spending keys are not written encrypted: bool, // In memory only (i.e, this field is not written to disk). Is the wallet unlocked and are // the spending keys present to allow spending from this wallet? unlocked: bool, enc_seed: [u8; 48], // If locked, this contains the encrypted seed nonce: Vec, // Nonce used to encrypt the wallet. seed: [u8; 32], // Seed phrase for this wallet. If wallet is locked, this is 0 // List of keys, actually in this wallet. This is a combination of HD keys derived from the seed, // viewing keys and imported spending keys. zkeys: Arc>>, // Transparent keys. tkeys: Arc>>, blocks: Arc>>, pub txs: Arc>>, // Transactions that are only in the mempool, but haven't been confirmed yet. // This is not stored to disk. pub mempool_txs: Arc>>, // The block at which this wallet was born. Rescans // will start from here. birthday: u64, // Non-serialized fields config: LightClientConfig, pub total_scan_duration: Arc>>, } impl LightWallet { pub fn serialized_version() -> u64 { return 9; } fn get_taddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> SecretKey { assert_eq!(bip39_seed.len(), 64); let ext_t_key = ExtendedPrivKey::with_seed(bip39_seed).unwrap(); ext_t_key .derive_private_key(KeyIndex::hardened_from_normalize_index(44).unwrap()).unwrap() .derive_private_key(KeyIndex::hardened_from_normalize_index(config.get_coin_type()).unwrap()).unwrap() .derive_private_key(KeyIndex::hardened_from_normalize_index(0).unwrap()).unwrap() .derive_private_key(KeyIndex::Normal(0)).unwrap() .derive_private_key(KeyIndex::Normal(pos)).unwrap() .private_key } fn get_zaddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> (ExtendedSpendingKey, ExtendedFullViewingKey, PaymentAddress) { assert_eq!(bip39_seed.len(), 64); let extsk: ExtendedSpendingKey = ExtendedSpendingKey::from_path( &ExtendedSpendingKey::master(bip39_seed), &[ ChildIndex::Hardened(32), ChildIndex::Hardened(config.get_coin_type()), ChildIndex::Hardened(pos) ], ); let extfvk = ExtendedFullViewingKey::from(&extsk); let address = extfvk.default_address().unwrap().1; (extsk, extfvk, address) } fn get_sietch_from_bip39seed( bip39_seed: &[u8]) -> PaymentAddress { assert_eq!(bip39_seed.len(), 64); let zdustextsk: ExtendedSpendingKey = ExtendedSpendingKey::from_path( &ExtendedSpendingKey::master(bip39_seed), &[ ChildIndex::Hardened(32), ], ); let zdustextfvk = ExtendedFullViewingKey::from(&zdustextsk); let zdustaddress = zdustextfvk.default_address().unwrap().1; zdustaddress } pub fn is_shielded_address(addr: &String, config: &LightClientConfig) -> bool { match address::RecipientAddress::from_str(addr, config.hrp_sapling_address(), config.base58_pubkey_address(), config.base58_script_address()) { Some(address::RecipientAddress::Shielded(_)) => true, _ => false, } } pub fn new(seed_phrase: Option, config: &LightClientConfig, latest_block: u64, number: u64) -> io::Result { // This is the source entropy that corresponds to the 24-word seed phrase let mut seed_bytes = [0u8; 32]; if seed_phrase.is_none() { // Create a random seed. let mut system_rng = OsRng; system_rng.fill(&mut seed_bytes); } else { let phrase = match Mnemonic::from_phrase(seed_phrase.clone().unwrap(), Language::English) { Ok(p) => p, Err(e) => { let e = format!("Error parsing phrase: {}", e); error!("{}", e); return Err(io::Error::new(ErrorKind::InvalidData, e)); } }; seed_bytes.copy_from_slice(&phrase.entropy()); } // The seed bytes is the raw entropy. To pass it to HD wallet generation, // we need to get the 64 byte bip39 entropy let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&seed_bytes, Language::English).unwrap(), ""); // Derive only the first sk and address let tpk = LightWallet::get_taddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0); let taddr = LightWallet::address_from_prefix_sk(&config.base58_pubkey_address(), &tpk); // TODO: We need to monitor addresses, and always keep 1 "free" address, so // users can import a seed phrase and automatically get all used addresses let hdkey_num = 0; let (extsk, _, _) = LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed.as_bytes(), hdkey_num); let lw = LightWallet { encrypted: false, unlocked: true, enc_seed: [0u8; 48], nonce: vec![], seed: seed_bytes, zkeys: Arc::new(RwLock::new(vec![WalletZKey::new_hdkey(hdkey_num, extsk)])), tkeys: Arc::new(RwLock::new(vec![WalletTKey::new_hdkey(tpk, taddr)])), blocks: Arc::new(RwLock::new(vec![])), txs: Arc::new(RwLock::new(HashMap::new())), mempool_txs: Arc::new(RwLock::new(HashMap::new())), config: config.clone(), birthday: latest_block, total_scan_duration: Arc::new(RwLock::new(vec![Duration::new(0, 0)])), }; // If restoring from seed, make sure we are creating 50 addresses for users if seed_phrase.is_some() { for _i in 0..number { lw.add_zaddr(); } for _i in 0..5 { lw.add_taddr(); } } Ok(lw) } pub fn read(mut inp: R, config: &LightClientConfig) -> io::Result { let version = inp.read_u64::()?; if version > LightWallet::serialized_version() { let e = format!("Don't know how to read wallet version {}. Do you have the latest version?", version); error!("{}", e); return Err(io::Error::new(ErrorKind::InvalidData, e)); } println!("Reading wallet version {}", version); info!("Reading wallet version {}", version); // At version 5, we're writing the rest of the file as a compressed stream (gzip) let mut reader: Box = if version !=5 { info!("Reading direct"); Box::new(inp) } else { info!("Reading libflat"); Box::new(Decoder::new(inp).unwrap()) }; let encrypted = if version >= 4 { reader.read_u8()? > 0 } else { false }; info!("Wallet Encryption {:?}", encrypted); let mut enc_seed = [0u8; 48]; if version >= 4 { reader.read_exact(&mut enc_seed)?; } let nonce = if version >= 4 { Vector::read(&mut reader, |r| r.read_u8())? } else { vec![] }; // Seed let mut seed_bytes = [0u8; 32]; reader.read_exact(&mut seed_bytes)?; let zkeys = if version <= 6 { // Up until version 6, the wallet keys were written out individually // Read the spending keys let extsks = Vector::read(&mut reader, |r| ExtendedSpendingKey::read(r))?; let extfvks = if version >= 4 { // Read the viewing keys Vector::read(&mut reader, |r| ExtendedFullViewingKey::read(r))? } else { // Calculate the viewing keys extsks.iter().map(|sk| ExtendedFullViewingKey::from(sk)) .collect::>() }; // Calculate the addresses let addresses = extfvks.iter().map( |fvk| fvk.default_address().unwrap().1 ) .collect::>>(); // If extsks is of len 0, then this wallet is locked let zkeys_result = if extsks.len() == 0 { // Wallet is locked, so read only the viewing keys. extfvks.iter().zip(addresses.iter()).enumerate().map(|(i, (extfvk, payment_address))| { let zk = WalletZKey::new_locked_hdkey(i as u32, extfvk.clone()); if zk.zaddress != *payment_address { Err(io::Error::new(ErrorKind::InvalidData, "Payment address didn't match")) } else { Ok(zk) } }).collect::>>() } else { // Wallet is unlocked, read the spending keys as well extsks.into_iter().zip(extfvks.into_iter().zip(addresses.iter())).enumerate() .map(|(i, (extsk, (extfvk, payment_address)))| { let zk = WalletZKey::new_hdkey(i as u32, extsk); if zk.zaddress != *payment_address { return Err(io::Error::new(ErrorKind::InvalidData, "Payment address didn't match")); } if zk.extfvk != extfvk { return Err(io::Error::new(ErrorKind::InvalidData, "Full View key didn't match")); } Ok(zk) }).collect::>>() }; // Convert vector of results into result of vector, returning an error if any one of the keys failed the checks above zkeys_result.into_iter().collect::>()? } else { // After version 6, we read the WalletZKey structs directly Vector::read(&mut reader, |r| WalletZKey::read(r))? }; // Calculate the addresses let wallet_tkeys = if version >= 9 { Vector::read(&mut reader, |r| { WalletTKey::read(r) })? } else { let tkeys = Vector::read(&mut reader, |r| { let mut tpk_bytes = [0u8; 32]; r.read_exact(&mut tpk_bytes)?; secp256k1::SecretKey::from_slice(&tpk_bytes).map_err(|e| io::Error::new(ErrorKind::InvalidData, e)) })?; let taddresses = if version >= 4 { // Read the addresses Vector::read(&mut reader, |r| utils::read_string(r))? } else { // Calculate the addresses tkeys.iter().map(|sk| LightWallet::address_from_prefix_sk(&config.base58_pubkey_address(), sk)).collect() }; tkeys.iter().zip(taddresses.iter()).map(|(k, a)| WalletTKey::new_hdkey(*k, a.clone()) ).collect() }; let blocks = Vector::read(&mut reader, |r| BlockData::read(r))?; let txs_tuples = Vector::read(&mut reader, |r| { let mut txid_bytes = [0u8; 32]; r.read_exact(&mut txid_bytes)?; Ok((TxId{0: txid_bytes}, WalletTx::read(r).unwrap())) })?; let txs = txs_tuples.into_iter().collect::>(); let chain_name = utils::read_string(&mut reader)?; if chain_name != config.chain_name { return Err(Error::new(ErrorKind::InvalidData, format!("Wallet chain name {} doesn't match expected {}", chain_name, config.chain_name))); } let birthday = reader.read_u64::()?; Ok(LightWallet{ encrypted: encrypted, unlocked: !encrypted, // When reading from disk, if wallet is encrypted, it starts off locked. enc_seed: enc_seed, nonce: nonce, seed: seed_bytes, zkeys: Arc::new(RwLock::new(zkeys)), tkeys: Arc::new(RwLock::new(wallet_tkeys)), blocks: Arc::new(RwLock::new(blocks)), txs: Arc::new(RwLock::new(txs)), mempool_txs: Arc::new(RwLock::new(HashMap::new())), config: config.clone(), birthday, total_scan_duration: Arc::new(RwLock::new(vec![Duration::new(0, 0)])), }) } pub fn write(&self, mut writer: W) -> io::Result<()> { if self.encrypted && self.unlocked { return Err(Error::new(ErrorKind::InvalidInput, format!("Cannot write while wallet is unlocked while encrypted."))); } // Write the version writer.write_u64::(LightWallet::serialized_version())?; // Write if it is locked writer.write_u8(if self.encrypted {1} else {0})?; // Write the encrypted seed bytes writer.write_all(&self.enc_seed)?; // Write the nonce Vector::write(&mut writer, &self.nonce, |w, b| w.write_u8(*b))?; // Write the seed writer.write_all(&self.seed)?; // Flush after writing the seed, so in case of a disaster, we can still recover the seed. writer.flush()?; // Write all the wallet's keys Vector::write(&mut writer, &self.zkeys.read().unwrap(), |w, zk| zk.write(w) )?; // Write the transparent private keys Vector::write(&mut writer, &self.tkeys.read().unwrap(), |w, tk| tk.write(w) )?; Vector::write(&mut writer, &self.blocks.read().unwrap(), |w, b| b.write(w))?; // The hashmap, write as a set of tuples. Store them sorted so that wallets are // deterministically saved { let txlist = self.txs.read().unwrap(); let mut txns = txlist.iter().collect::>(); txns.sort_by(|a, b| a.0.partial_cmp(b.0).unwrap()); Vector::write(&mut writer, &txns, |w, (k, v)| { w.write_all(&k.0)?; v.write(w) })?; } utils::write_string(&mut writer, &self.config.chain_name)?; // While writing the birthday, get it from the fn so we recalculate it properly // in case of rescans etc... writer.write_u64::(self.get_birthday()) } pub fn note_address(hrp: &str, note: &SaplingNoteData) -> Option { match note.extfvk.fvk.vk.into_payment_address(note.diversifier, &JUBJUB) { Some(pa) => Some(encode_payment_address(hrp, &pa)), None => None } } pub fn get_birthday(&self) -> u64 { if self.birthday == 0 { self.get_first_tx_block() } else { cmp::min(self.get_first_tx_block(), self.birthday) } } // Get the first block that this wallet has a tx in. This is often used as the wallet's "birthday" // If there are no Txns, then the actual birthday (which is recorder at wallet creation) is returned // If no birthday was recorded, return the sapling activation height pub fn get_first_tx_block(&self) -> u64 { // Find the first transaction let mut blocks = self.txs.read().unwrap().values() .map(|wtx| wtx.block as u64) .collect::>(); blocks.sort(); *blocks.first() // Returns optional, so if there's no txns, it'll get the activation height .unwrap_or(&cmp::max(self.birthday, self.config.sapling_activation_height)) } // Get all z-address private keys. Returns a Vector of (address, privatekey, viewkey) pub fn get_z_private_keys(&self) -> Vec<(String, String, String)> { let keys = self.zkeys.read().unwrap().iter().map(|k| { let pkey = match k.extsk.clone().map(|extsk| encode_extended_spending_key(self.config.hrp_sapling_private_key(), &extsk)) { Some(pk) => pk, None => "".to_string() }; let vkey = encode_extended_full_viewing_key(self.config.hrp_sapling_viewing_key(), &k.extfvk); (encode_payment_address(self.config.hrp_sapling_address(),&k.zaddress), pkey, vkey) }).collect::>(); keys } /// Get all t-address private keys. Returns a Vector of (address, secretkey) pub fn get_t_secret_keys(&self) -> Vec<(String, String)> { self.tkeys.read().unwrap().iter().map(|wtk| { let sk = if wtk.tkey.is_some() { wtk.tkey.unwrap()[..].to_base58check(&self.config.base58_secretkey_prefix(), &[0x01]) } else { "".to_string() }; (wtk.address.clone(), sk) }).collect::>() } /// Adds a new z address to the wallet. This will derive a new address from the seed /// at the next position and add it to the wallet. /// NOTE: This does NOT rescan pub fn add_zaddr(&self) -> String { if !self.unlocked { return "Error: Can't add key while wallet is locked".to_string(); } if self.encrypted { return "Error: Can't add key while wallet is encrypted".to_string(); } // Find the highest pos we have let pos = self.zkeys.read().unwrap().iter() .filter(|zk| zk.hdkey_num.is_some()) .max_by(|zk1, zk2| zk1.hdkey_num.unwrap().cmp(&zk2.hdkey_num.unwrap())) .map_or(0, |zk| zk.hdkey_num.unwrap() + 1); let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), ""); let (extsk, _, _) = LightWallet::get_zaddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos); // let zaddr = encode_payment_address(self.config.hrp_sapling_address(), &address); let newkey = WalletZKey::new_hdkey(pos, extsk); self.zkeys.write().unwrap().push(newkey.clone()); encode_payment_address(self.config.hrp_sapling_address(), &newkey.zaddress) } // Add a new Sietch Addr. This will derive a new zdust address from manipluated seed pub fn add_zaddrdust(&self) -> String { let mut seed_bytes = [0u8; 32]; // Use random generator to create a new Sietch seed let mut rng = rand::thread_rng(); let letter: String = rng.gen_range(b'A', b'Z').to_string(); let number: String = rng.gen_range(0, 999999).to_string(); let s = format!("{}{:06}", letter, number); let my_string = String::from(s); let dust: &str = &my_string; let mut system_rng = OsRng; system_rng.fill(&mut seed_bytes); let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&seed_bytes, Language::English).unwrap(), dust); let zdustaddress = LightWallet::get_sietch_from_bip39seed(&bip39_seed.as_bytes()); let zdust = encode_payment_address("zs", &zdustaddress); zdust } /// Add a new t address to the wallet. This will derive a new address from the seed /// at the next position. /// NOTE: This will not rescan the wallet pub fn add_taddr(&self) -> String { if !self.unlocked { return "Error: Can't add key while wallet is locked".to_string(); } if self.encrypted { return "Error: Can't add key while wallet is encrypted".to_string(); } let pos = self.tkeys.read().unwrap().len() as u32; let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), ""); let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos); let address = self.address_from_sk(&sk); self.tkeys.write().unwrap().push(WalletTKey::new_hdkey(sk, address.clone())); address } pub fn import_taddr(&mut self, sk: String, birthday: u64) -> String { if !self.unlocked { return "Error: Can't add key while wallet is locked".to_string(); } //// Decode Wif to base58 to hex let sk_to_bs58 = bs58::decode(sk).into_vec().unwrap(); let bs58_to_hex = hex::encode(sk_to_bs58); //// Manipulate string, to exclude last 4 bytes (checksum bytes), first 2 bytes (secretkey prefix) and the compressed flag (works only for compressed Wifs!) let slice_sk = &bs58_to_hex[2..66]; //// Get the SecretKey from slice let secret_key = SecretKey::from_slice(&hex::decode(slice_sk).unwrap()); let sk_raw = secret_key.unwrap(); //// Make sure the key doesn't already exist if self.tkeys.read().unwrap().iter().find(|&wk| wk.tkey.is_some() && wk.tkey.as_ref().unwrap() == &sk_raw.clone()).is_some() { return "Error: Key already exists".to_string(); } //// Get the taddr from key let address = self.address_from_sk(&sk_raw); //// Add to tkeys self.tkeys.write().unwrap().push(WalletTKey::import_hdkey(sk_raw , address.clone())); // Adjust wallet birthday if birthday < self.birthday { self.birthday = if birthday < self.config.sapling_activation_height {self.config.sapling_activation_height} else {birthday}; } address } // Add a new imported spending key to the wallet /// NOTE: This will not rescan the wallet pub fn add_imported_sk(&mut self, sk: String, birthday: u64) -> String { if !self.unlocked { return "Error: Can't add key while wallet is locked".to_string(); } // First, try to interpret the key let extsk = match decode_extended_spending_key(self.config.hrp_sapling_private_key(), &sk) { Ok(Some(k)) => k, Ok(None) => return format!("Error: Couldn't decode spending key"), Err(e) => return format!("Error importing spending key: {}", e) }; // Make sure the key doesn't already exist if self.zkeys.read().unwrap().iter().find(|&wk| wk.extsk.is_some() && wk.extsk.as_ref().unwrap() == &extsk.clone()).is_some() { return "Error: Key already exists".to_string(); } let extfvk = ExtendedFullViewingKey::from(&extsk); let zaddress = { let mut zkeys = self.zkeys.write().unwrap(); let maybe_existing_zkey = zkeys.iter_mut().find(|wk| wk.extfvk == extfvk); // If the viewing key exists, and is now being upgraded to the spending key, replace it in-place if maybe_existing_zkey.is_some() { let mut existing_zkey = maybe_existing_zkey.unwrap(); existing_zkey.extsk = Some(extsk); existing_zkey.keytype = WalletZKeyType::ImportedSpendingKey; existing_zkey.zaddress.clone() } else { let newkey = WalletZKey::new_imported_sk(extsk); zkeys.push(newkey.clone()); newkey.zaddress } }; // Adjust wallet birthday if birthday < self.birthday { self.birthday = if birthday < self.config.sapling_activation_height {self.config.sapling_activation_height} else {birthday}; } encode_payment_address(self.config.hrp_sapling_address(), &zaddress) } // Add a new imported viewing key to the wallet /// NOTE: This will not rescan the wallet pub fn add_imported_vk(&mut self, vk: String, birthday: u64) -> String { if !self.unlocked { return "Error: Can't add key while wallet is locked".to_string(); } // First, try to interpret the key let extfvk = match decode_extended_full_viewing_key(self.config.hrp_sapling_viewing_key(), &vk) { Ok(Some(k)) => k, Ok(None) => return format!("Error: Couldn't decode viewing key"), Err(e) => return format!("Error importing viewing key: {}", e) }; // Make sure the key doesn't already exist if self.zkeys.read().unwrap().iter().find(|wk| wk.extfvk == extfvk.clone()).is_some() { return "Error: Key already exists".to_string(); } let newkey = WalletZKey::new_imported_viewkey(extfvk); self.zkeys.write().unwrap().push(newkey.clone()); // Adjust wallet birthday if birthday < self.birthday { self.birthday = if birthday < self.config.sapling_activation_height {self.config.sapling_activation_height} else {birthday}; } encode_payment_address(self.config.hrp_sapling_address(), &newkey.zaddress) } /// Clears all the downloaded blocks and resets the state back to the initial block. /// After this, the wallet's initial state will need to be set /// and the wallet will need to be rescanned pub fn clear_blocks(&self) { self.blocks.write().unwrap().clear(); self.txs.write().unwrap().clear(); self.mempool_txs.write().unwrap().clear(); } pub fn set_initial_block(&self, height: i32, hash: &str, sapling_tree: &str) -> bool { let mut blocks = self.blocks.write().unwrap(); if !blocks.is_empty() { return false; } let hash = match hex::decode(hash) { Ok(hash) => { let mut r = hash; r.reverse(); BlockHash::from_slice(&r) }, Err(e) => { eprintln!("{}", e); return false; } }; let sapling_tree = match hex::decode(sapling_tree) { Ok(tree) => tree, Err(e) => { eprintln!("{}", e); return false; } }; if let Ok(tree) = CommitmentTree::read(&sapling_tree[..]) { blocks.push(BlockData { height, hash, tree }); true } else { false } } // Get the latest sapling commitment tree. It will return the height and the hex-encoded sapling commitment tree at that height pub fn get_sapling_tree(&self) -> Result<(i32, String, String), String> { let blocks = self.blocks.read().unwrap(); let block = match blocks.last() { Some(block) => block, None => return Err("Couldn't get a block height!".to_string()) }; let mut write_buf = vec![]; block.tree.write(&mut write_buf).map_err(|e| format!("Error writing commitment tree {}", e))?; let mut blockhash = vec![]; blockhash.extend_from_slice(&block.hash.0); blockhash.reverse(); Ok((block.height, hex::encode(blockhash), hex::encode(write_buf))) } pub fn last_scanned_height(&self) -> i32 { self.blocks.read().unwrap() .last() .map(|block| block.height) .unwrap_or(self.config.sapling_activation_height as i32 - 1) } /// Determines the target height for a transaction, and the offset from which to /// select anchors, based on the current synchronised block chain. fn get_target_height_and_anchor_offset(&self) -> Option<(u32, usize)> { match { let blocks = self.blocks.read().unwrap(); ( blocks.first().map(|block| block.height as u32), blocks.last().map(|block| block.height as u32), ) } { (Some(min_height), Some(max_height)) => { let target_height = max_height; // Select an anchor ANCHOR_OFFSET back from the target block, // unless that would be before the earliest block we have. let anchor_height = cmp::max(target_height.saturating_sub(self.config.anchor_offset), min_height); Some((target_height, (target_height - anchor_height) as usize)) } _ => None, } } /// Get the height of the anchor block pub fn get_anchor_height(&self) -> u32 { match self.get_target_height_and_anchor_offset() { Some((height, anchor_offset)) => height - anchor_offset as u32 - 1, None => return 0, } } pub fn get_all_taddresses(&self) -> Vec { self.tkeys.read().unwrap() .iter() .map(|wtx| wtx.address.clone()).collect() } pub fn get_all_zaddresses(&self) -> Vec { self.zkeys.read().unwrap().iter().map( |zk| { encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress) }).collect() } pub fn memo_str(memo: &Option) -> Option { match memo { Some(memo) => { match memo.to_utf8() { Some(Ok(memo_str)) => Some(memo_str), _ => None } } _ => None } } pub fn address_from_prefix_sk(prefix: &[u8; 1], sk: &secp256k1::SecretKey) -> String { let secp = secp256k1::Secp256k1::new(); let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk); // Encode into t address let mut hash160 = ripemd160::Ripemd160::new(); hash160.input(Sha256::digest(&pk.serialize()[..].to_vec())); hash160.result().to_base58check(prefix, &[]) } pub fn address_from_sk(&self, sk: &secp256k1::SecretKey) -> String { LightWallet::address_from_prefix_sk(&self.config.base58_pubkey_address(), sk) } pub fn address_from_pubkeyhash(&self, ta: Option) -> Option { match ta { Some(TransparentAddress::PublicKey(hash)) => { Some(hash.to_base58check(&self.config.base58_pubkey_address(), &[])) }, Some(TransparentAddress::Script(hash)) => { Some(hash.to_base58check(&self.config.base58_script_address(), &[])) }, _ => None } } pub fn get_seed_phrase(&self) -> String { if !self.unlocked { return "".to_string(); } Mnemonic::from_entropy(&self.seed, Language::English, ).unwrap().phrase().to_string() } pub fn encrypt(&mut self, passwd: String) -> io::Result<()> { if self.encrypted { return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is already encrypted")); } // Get the doublesha256 of the password, which is the right length let key = secretbox::Key::from_slice(&double_sha256(passwd.as_bytes())).unwrap(); let nonce = secretbox::gen_nonce(); let cipher = secretbox::seal(&self.seed, &nonce, &key); self.enc_seed.copy_from_slice(&cipher); self.nonce = nonce.as_ref().to_vec(); // Encrypt the individual keys self.tkeys.write().unwrap().iter_mut() .map(|k| k.encrypt(&key)) .collect::>>()?; self.zkeys.write().unwrap().iter_mut() .map(|k| k.encrypt(&key)) .collect::>>()?; self.encrypted = true; self.lock()?; Ok(()) } pub fn lock(&mut self) -> io::Result<()> { if !self.encrypted { return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is not encrypted")); } if !self.unlocked { return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is already locked")); } // Empty the seed and the secret keys self.seed.copy_from_slice(&[0u8; 32]); // Remove all the private key from the tkeys self.tkeys.write().unwrap().iter_mut().map(|tk| { tk.lock() }).collect::>>()?; // Remove all the private key from the zkeys self.zkeys.write().unwrap().iter_mut().map(|zk| { zk.lock() }).collect::>>()?; self.unlocked = false; Ok(()) } pub fn unlock(&mut self, passwd: String) -> io::Result<()> { if !self.encrypted { return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is not encrypted")); } if self.encrypted && self.unlocked { return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is already unlocked")); } // Get the doublesha256 of the password, which is the right length let key = secretbox::Key::from_slice(&double_sha256(passwd.as_bytes())).unwrap(); let nonce = secretbox::Nonce::from_slice(&self.nonce).unwrap(); let seed = match secretbox::open(&self.enc_seed, &nonce, &key) { Ok(s) => s, Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));} }; // Now that we have the seed, we'll generate the extsks and tkeys, and verify the fvks and addresses // respectively match // The seed bytes is the raw entropy. To pass it to HD wallet generation, // we need to get the 64 byte bip39 entropy let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&seed, Language::English).unwrap(), ""); // Go over the tkeys, and add the keys again self.tkeys.write().unwrap().iter_mut().map(|tk| { tk.unlock(&key) }).collect::>>()?; // Go over the zkeys, and add the spending keys again self.zkeys.write().unwrap().iter_mut().map(|zk| { zk.unlock(&self.config, bip39_seed.as_bytes(), &key) }).collect::>>()?; // Everything checks out, so we'll update our wallet with the decrypted values self.seed.copy_from_slice(&seed); self.encrypted = true; self.unlocked = true; Ok(()) } // Removing encryption means unlocking it and setting the self.encrypted = false, // permanantly removing the encryption pub fn remove_encryption(&mut self, passwd: String) -> io::Result<()> { if !self.encrypted { return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is not encrypted")); } // Unlock the wallet if it's locked if !self.unlocked { self.unlock(passwd)?; } // Remove encryption from individual tkeys self.tkeys.write().unwrap().iter_mut().map(|tk| { tk.remove_encryption() }).collect::>>()?; // Remove encryption from individual zkeys self.zkeys.write().unwrap().iter_mut().map(|zk| { zk.remove_encryption() }).collect::>>()?; // Permanantly remove the encryption self.encrypted = false; self.nonce = vec![]; self.enc_seed.copy_from_slice(&[0u8; 48]); Ok(()) } pub fn is_encrypted(&self) -> bool { return self.encrypted; } pub fn is_unlocked_for_spending(&self) -> bool { return self.unlocked; } pub fn zbalance(&self, addr: Option) -> u64 { self.txs.read().unwrap() .values() .map (|tx| { tx.notes.iter() .filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it. match addr.clone() { Some(a) => a == encode_payment_address( self.config.hrp_sapling_address(), &nd.extfvk.fvk.vk .into_payment_address(nd.diversifier, &JUBJUB).unwrap() ), None => true } }) .map(|nd| if nd.spent.is_none() { nd.note.value } else { 0 }) .sum::() }) .sum::() as u64 } // Get all (unspent) utxos. Unconfirmed spent utxos are included pub fn get_utxos(&self) -> Vec { let txs = self.txs.read().unwrap(); txs.values() .flat_map(|tx| { tx.utxos.iter().filter(|utxo| utxo.spent.is_none()) }) .map(|utxo| utxo.clone()) .collect::>() } pub fn tbalance(&self, addr: Option) -> u64 { self.get_utxos().iter() .filter(|utxo| { match addr.clone() { Some(a) => utxo.address == a, None => true, } }) .map(|utxo| utxo.value ) .sum::() as u64 } pub fn verified_zbalance(&self, addr: Option) -> u64 { let anchor_height = match self.get_target_height_and_anchor_offset() { Some((height, anchor_offset)) => height - anchor_offset as u32 , None => return 0, }; self.txs .read() .unwrap() .values() .map(|tx| { if tx.block as u32 <= anchor_height { tx.notes .iter() .filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none()) .filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it. match addr.clone() { Some(a) => a == encode_payment_address( self.config.hrp_sapling_address(), &nd.extfvk.fvk.vk .into_payment_address(nd.diversifier, &JUBJUB).unwrap() ), None => true } }) .map(|nd| nd.note.value) .sum::() } else { 0 } }) .sum::() } pub fn spendable_zbalance(&self, addr: Option) -> u64 { let anchor_height = self.get_anchor_height(); self.txs .read() .unwrap() .values() .map(|tx| { if tx.block as u32 <= anchor_height { tx.notes .iter() .filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none()) .filter(|nd| { // Check to see if we have this note's spending key. self.have_spendingkey_for_extfvk(&nd.extfvk) }) .filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it. match addr.clone() { Some(a) => a == encode_payment_address( self.config.hrp_sapling_address(), &nd.extfvk.fvk.vk .into_payment_address(nd.diversifier, &JUBJUB).unwrap() ), None => true } }) .map(|nd| nd.note.value) .sum::() } else { 0 } }) .sum::() as u64 } pub fn have_spendingkey_for_extfvk(&self, extfvk: &ExtendedFullViewingKey) -> bool { match self.zkeys.read().unwrap().iter().find(|zk| zk.extfvk == *extfvk) { None => false, Some(zk) => zk.have_spending_key() } } pub fn have_spending_key_for_zaddress(&self, address: &String) -> bool { match self.zkeys.read().unwrap().iter() .find(|zk| encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress) == *address) { None => false, Some(zk) => zk.have_spending_key() } } fn add_toutput_to_wtx(&self, height: i32, timestamp: u64, txid: &TxId, vout: &TxOut, n: u64) { let mut txs = self.txs.write().unwrap(); // Find the existing transaction entry, or create a new one. if !txs.contains_key(&txid) { let tx_entry = WalletTx::new(height, timestamp, &txid); txs.insert(txid.clone(), tx_entry); } let tx_entry = txs.get_mut(&txid).unwrap(); // Make sure the vout isn't already there. match tx_entry.utxos.iter().find(|utxo| { utxo.txid == *txid && utxo.output_index == n && Amount::from_u64(utxo.value).unwrap() == vout.value }) { Some(utxo) => { info!("Already have {}:{}", utxo.txid, utxo.output_index); } None => { let address = self.address_from_pubkeyhash(vout.script_pubkey.address()); if address.is_none() { error!("Couldn't determine address for output!"); } else { info!("Added to wallet {}:{}", txid, n); // Add the utxo tx_entry.utxos.push(Utxo { address: address.unwrap(), txid: txid.clone(), output_index: n, script: vout.script_pubkey.0.clone(), value: vout.value.into(), height, spent: None, unconfirmed_spent: None, }); } } } } // If one of the last 'n' taddress was used, ensure we add the next HD taddress to the wallet. pub fn ensure_hd_taddresses(&self, address: &String) { let last_addresses = { self.tkeys.read().unwrap() .iter() .map(|t| t.address.clone()) .rev().take(GAP_RULE_UNUSED_ADDRESSES).map(|s| s.clone()) .collect::>() }; match last_addresses.iter().position(|s| *s == *address) { None => { return; }, Some(pos) => { info!("Adding {} new zaddrs", (GAP_RULE_UNUSED_ADDRESSES - pos)); // If it in the last unused, addresses, create that many more for _ in 0..(GAP_RULE_UNUSED_ADDRESSES - pos) { // If the wallet is locked, this is a no-op. That is fine, since we really // need to only add new addresses when restoring a new wallet, when it will not be locked. // Also, if it is locked, the user can't create new addresses anyway. self.add_taddr(); } } } } // If one of the last 'n' zaddress was used, ensure we add the next HD zaddress to the wallet pub fn ensure_hd_zaddresses(&self, address: &String) { let last_addresses = { self.zkeys.read().unwrap().iter() .filter(|zk| zk.keytype == WalletZKeyType::HdKey) .rev() .take(GAP_RULE_UNUSED_ADDRESSES) .map(|s| encode_payment_address(self.config.hrp_sapling_address(), &s.zaddress)) .collect::>() }; match last_addresses.iter().position(|s| *s == *address) { None => { return; }, Some(pos) => { info!("Adding {} new zaddrs", (GAP_RULE_UNUSED_ADDRESSES - pos)); // If it in the last unused, addresses, create that many more for _ in 0..(GAP_RULE_UNUSED_ADDRESSES - pos) { // If the wallet is locked, this is a no-op. That is fine, since we really // need to only add new addresses when restoring a new wallet, when it will not be locked. // Also, if it is locked, the user can't create new addresses anyway. self.add_zaddr(); } } } } // Scan the full Tx and update memos for incoming shielded transactions. pub fn scan_full_tx(&self, tx: &Transaction, height: i32, datetime: u64) { let mut total_transparent_spend: u64 = 0; // Scan all the inputs to see if we spent any transparent funds in this tx for vin in tx.vin.iter() { // Find the txid in the list of utxos that we have. let txid = TxId {0: vin.prevout.hash}; match self.txs.write().unwrap().get_mut(&txid) { Some(wtx) => { //println!("Looking for {}, {}", txid, vin.prevout.n); // One of the tx outputs is a match let spent_utxo = wtx.utxos.iter_mut() .find(|u| u.txid == txid && u.output_index == (vin.prevout.n as u64)); match spent_utxo { Some(su) => { info!("Spent utxo from {} was spent in {}", txid, tx.txid()); su.spent = Some(tx.txid().clone()); su.unconfirmed_spent = None; total_transparent_spend += su.value; }, _ => {} } }, _ => {} }; } if total_transparent_spend > 0 { // Update the WalletTx. Do it in a short scope because of the write lock. let mut txs = self.txs.write().unwrap(); if !txs.contains_key(&tx.txid()) { let tx_entry = WalletTx::new(height, datetime, &tx.txid()); txs.insert(tx.txid().clone(), tx_entry); } txs.get_mut(&tx.txid()).unwrap() .total_transparent_value_spent = total_transparent_spend; } // Scan for t outputs let all_taddresses = self.tkeys.read().unwrap().iter() .map(|wtx| wtx.address.clone()) .map(|a| a.clone()) .collect::>(); for address in all_taddresses { for (n, vout) in tx.vout.iter().enumerate() { match vout.script_pubkey.address() { Some(TransparentAddress::PublicKey(hash)) => { if address == hash.to_base58check(&self.config.base58_pubkey_address(), &[]) { // This is our address. Add this as an output to the txid self.add_toutput_to_wtx(height, datetime, &tx.txid(), &vout, n as u64); // Ensure that we add any new HD addresses self.ensure_hd_taddresses(&address); } }, _ => {} } } } { let total_shielded_value_spent = self.txs.read().unwrap().get(&tx.txid()).map_or(0, |wtx| wtx.total_shielded_value_spent); if total_transparent_spend + total_shielded_value_spent > 0 { // We spent money in this Tx, so grab all the transparent outputs (except ours) and add them to the // outgoing metadata // Collect our t-addresses let wallet_taddrs = self.tkeys.read().unwrap().iter() .map(|wtx| wtx.address.clone()) .map(|a| a.clone()) .collect::>(); for vout in tx.vout.iter() { let taddr = self.address_from_pubkeyhash(vout.script_pubkey.address()); if taddr.is_some() && !wallet_taddrs.contains(&taddr.clone().unwrap()) { let taddr = taddr.unwrap(); // Add it to outgoing metadata let mut txs = self.txs.write().unwrap(); if txs.get(&tx.txid()).unwrap().outgoing_metadata.iter() .find(|om| om.address == taddr && Amount::from_u64(om.value).unwrap() == vout.value) .is_some() { warn!("Duplicate outgoing metadata"); continue; } // Write the outgoing metadata txs.get_mut(&tx.txid()).unwrap() .outgoing_metadata .push(OutgoingTxMetadata{ address: taddr, value: vout.value.into(), memo: Memo::default(), }); } } } } // Scan shielded sapling outputs to see if anyone of them is us, and if it is, extract the memo for output in tx.shielded_outputs.iter() { let ivks: Vec<_> = self.zkeys.read().unwrap().iter() .map(|zk| zk.extfvk.fvk.vk.ivk() ).collect(); let cmu = output.cmu; let ct = output.enc_ciphertext; // Search all of our keys for ivk in ivks { let epk_prime = output.ephemeral_key.as_prime_order(&JUBJUB).unwrap(); let (note, _to, memo) = match try_sapling_note_decryption(&ivk, &epk_prime, &cmu, &ct) { Some(ret) => ret, None => continue, }; if memo.to_utf8().is_some() { // info!("A sapling note was sent to wallet in {} that had a memo", tx.txid()); // Do it in a short scope because of the write lock. let mut txs = self.txs.write().unwrap(); // Update memo if we have this Tx. match txs.get_mut(&tx.txid()) .and_then(|t| { t.notes.iter_mut().find(|nd| nd.note == note) }) { None => { info!("No txid matched for incoming sapling funds while updating memo"); () }, Some(nd) => { nd.memo = Some(memo) } } } } // Also scan the output to see if it can be decoded with our OutgoingViewKey // If it can, then we sent this transaction, so we should be able to get // the memo and value for our records // First, collect all our z addresses, to check for change // Collect z addresses let z_addresses = self.zkeys.read().unwrap().iter().map( |zk| { encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress) }).collect::>(); // Search all ovks that we have let ovks: Vec<_> = self.zkeys.read().unwrap().iter() .map(|zk| zk.extfvk.fvk.ovk.clone()) .collect(); for ovk in ovks { match try_sapling_output_recovery( &ovk, &output.cv, &output.cmu, &output.ephemeral_key.as_prime_order(&JUBJUB).unwrap(), &output.enc_ciphertext, &output.out_ciphertext) { Some((note, payment_address, memo)) => { let address = encode_payment_address(self.config.hrp_sapling_address(), &payment_address); // Check if this is change, and if it also doesn't have a memo, don't add // to the outgoing metadata. // If this is change (i.e., funds sent to ourself) AND has a memo, then // presumably the users is writing a memo to themself, so we will add it to // the outgoing metadata, even though it might be confusing in the UI, but hopefully // the user can make sense of it. if z_addresses.contains(&address) && memo.to_utf8().is_none() { continue; } // Update the WalletTx // Do it in a short scope because of the write lock. { info!("A sapling output was sent in {}", tx.txid()); let mut txs = self.txs.write().unwrap(); if txs.get(&tx.txid()).unwrap().outgoing_metadata.iter() .find(|om| om.address == address && om.value == note.value && om.memo == memo) .is_some() { warn!("Duplicate outgoing metadata"); continue; } // Write the outgoing metadata txs.get_mut(&tx.txid()).unwrap() .outgoing_metadata .push(OutgoingTxMetadata{ address, value: note.value, memo, }); } }, None => {} }; } } // Mark this Tx as scanned { let mut txs = self.txs.write().unwrap(); match txs.get_mut(&tx.txid()) { Some(wtx) => wtx.full_tx_scanned = true, None => {}, }; } } // Invalidate all blocks including and after "at_height". // Returns the number of blocks invalidated pub fn invalidate_block(&self, at_height: i32) -> u64 { let mut num_invalidated = 0; // First remove the blocks { let mut blks = self.blocks.write().unwrap(); while blks.last().unwrap().height >= at_height { blks.pop(); num_invalidated += 1; } } // Next, remove entire transactions { let mut txs = self.txs.write().unwrap(); let txids_to_remove = txs.values() .filter_map(|wtx| if wtx.block >= at_height {Some(wtx.txid.clone())} else {None}) .collect::>(); for txid in &txids_to_remove { txs.remove(&txid); } // We also need to update any sapling note data and utxos in existing transactions that // were spent in any of the txids that were removed txs.values_mut() .for_each(|wtx| { wtx.notes.iter_mut() .for_each(|nd| { if nd.spent.is_some() && txids_to_remove.contains(&nd.spent.unwrap()) { nd.spent = None; } if nd.unconfirmed_spent.is_some() && txids_to_remove.contains(&nd.spent.unwrap()) { nd.unconfirmed_spent = None; } }) }) } // Of the notes that still remain, unroll the witness. // Remove `num_invalidated` items from the witness { let mut txs = self.txs.write().unwrap(); // Trim all witnesses for the invalidated blocks for tx in txs.values_mut() { for nd in tx.notes.iter_mut() { let _discard = nd.witnesses.split_off(nd.witnesses.len().saturating_sub(num_invalidated)); } } } num_invalidated as u64 } /// Scans a [`CompactOutput`] with a set of [`ExtendedFullViewingKey`]s. /// /// Returns a [`WalletShieldedOutput`] and corresponding [`IncrementalWitness`] if this /// output belongs to any of the given [`ExtendedFullViewingKey`]s. /// /// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are incremented /// with this output's commitment. fn scan_output_internal( &self, (index, output): (usize, CompactOutput), ivks: &[Fs], tree: &mut CommitmentTree, existing_witnesses: &mut [&mut IncrementalWitness], block_witnesses: &mut [&mut IncrementalWitness], new_witnesses: &mut [&mut IncrementalWitness], pool: &ThreadPool ) -> Option { let cmu = output.cmu().ok()?; let epk = output.epk().ok()?; let ct = output.ciphertext; let (tx, rx) = channel(); ivks.iter().enumerate().for_each(|(account, ivk)| { // Clone all values for passing to the closure let ivk = ivk.clone(); let epk = epk.clone(); let ct = ct.clone(); let tx = tx.clone(); pool.execute(move || { let m = try_sapling_compact_note_decryption(&ivk, &epk, &cmu, &ct); let r = match m { Some((note, to)) => { tx.send(Some(Some((note, to, account)))) }, None => { tx.send(Some(None)) } }; match r { Ok(_) => {}, Err(e) => println!("Send error {:?}", e) } }); }); // Increment tree and witnesses let node = Node::new(cmu.into()); for witness in existing_witnesses { witness.append(node).unwrap(); } for witness in block_witnesses { witness.append(node).unwrap(); } for witness in new_witnesses { witness.append(node).unwrap(); } tree.append(node).unwrap(); // Collect all the RXs and fine if there was a valid result somewhere let mut wsos = vec![]; for _i in 0..ivks.len() { let n = rx.recv().unwrap(); let epk = epk.clone(); let wso = match n { None => panic!("Got a none!"), Some(None) => None, Some(Some((note, to, account))) => { // A note is marked as "change" if the account that received it // also spent notes in the same transaction. This will catch, // for instance: // - Change created by spending fractions of notes. // - Notes created by consolidation transactions. // - Notes sent from one account to itself. //let is_change = spent_from_accounts.contains(&account); Some(WalletShieldedOutput { index, cmu, epk, account, note, to, is_change: false, witness: IncrementalWitness::from_tree(tree), }) } }; wsos.push(wso); } match wsos.into_iter().find(|wso| wso.is_some()) { Some(Some(wso)) => Some(wso), _ => None } } /// Scans a [`CompactBlock`] with a set of [`ExtendedFullViewingKey`]s. /// /// Returns a vector of [`WalletTx`]s belonging to any of the given /// [`ExtendedFullViewingKey`]s, and the corresponding new [`IncrementalWitness`]es. /// /// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are /// incremented appropriately. pub fn scan_block_internal( &self, block: CompactBlock, extfvks: &[ExtendedFullViewingKey], nullifiers: Vec<(Vec, usize)>, tree: &mut CommitmentTree, existing_witnesses: &mut [&mut IncrementalWitness], pool: &ThreadPool ) -> Vec { let mut wtxs: Vec = vec![]; let ivks = extfvks.iter().map(|extfvk| extfvk.fvk.vk.ivk()).collect::>(); for tx in block.vtx.into_iter() { let num_spends = tx.spends.len(); let num_outputs = tx.outputs.len(); let (ctx, crx) = channel(); { let nullifiers = nullifiers.clone(); let tx = tx.clone(); pool.execute(move || { // Check for spent notes // The only step that is not constant-time is the filter() at the end. let shielded_spends: Vec<_> = tx .spends .into_iter() .enumerate() .map(|(index, spend)| { // Find the first tracked nullifier that matches this spend, and produce // a WalletShieldedSpend if there is a match, in constant time. nullifiers .iter() .map(|(nf, account)| CtOption::new(*account as u64, nf.ct_eq(&spend.nf[..]))) .fold(CtOption::new(0, 0.into()), |first, next| { CtOption::conditional_select(&next, &first, first.is_some()) }) .map(|account| WalletShieldedSpend { index, nf: spend.nf, account: account as usize, }) }) .filter(|spend| spend.is_some().into()) .map(|spend| spend.unwrap()) .collect(); // Collect the set of accounts that were spent from in this transaction let spent_from_accounts: HashSet<_> = shielded_spends.iter().map(|spend| spend.account).collect(); ctx.send((shielded_spends, spent_from_accounts)).unwrap(); drop(ctx); }); } // Check for incoming notes while incrementing tree and witnesses let mut shielded_outputs: Vec = vec![]; { // Grab mutable references to new witnesses from previous transactions // in this block so that we can update them. Scoped so we don't hold // mutable references to wtxs for too long. let mut block_witnesses: Vec<_> = wtxs .iter_mut() .map(|tx| { tx.shielded_outputs .iter_mut() .map(|output| &mut output.witness) }) .flatten() .collect(); for to_scan in tx.outputs.into_iter().enumerate() { // Grab mutable references to new witnesses from previous outputs // in this transaction so that we can update them. Scoped so we // don't hold mutable references to shielded_outputs for too long. let mut new_witnesses: Vec<_> = shielded_outputs .iter_mut() .map(|output| &mut output.witness) .collect(); if let Some(output) = self.scan_output_internal( to_scan, &ivks, tree, existing_witnesses, &mut block_witnesses, &mut new_witnesses, pool ) { shielded_outputs.push(output); } } } let (shielded_spends, spent_from_accounts) = crx.recv().unwrap(); // Identify change outputs shielded_outputs.iter_mut().for_each(|output| { if spent_from_accounts.contains(&output.account) { output.is_change = true; } }); // Update wallet tx if !(shielded_spends.is_empty() && shielded_outputs.is_empty()) { let mut txid = TxId([0u8; 32]); txid.0.copy_from_slice(&tx.hash); wtxs.push(zcash_client_backend::wallet::WalletTx { txid, index: tx.index as usize, num_spends, num_outputs, shielded_spends, shielded_outputs, }); } } wtxs } pub fn scan_block(&self, block_bytes: &[u8]) -> Result, i32> { self.scan_block_with_pool(&block_bytes, &ThreadPool::new(1)) } // Scan a block. Will return an error with the block height that failed to scan pub fn scan_block_with_pool(&self, block_bytes: &[u8], pool: &ThreadPool) -> Result, i32> { let block: CompactBlock = match parse_from_bytes(block_bytes) { Ok(block) => block, Err(e) => { error!("Could not parse CompactBlock from bytes: {}", e); return Err(-1); } }; // Scanned blocks MUST be height-sequential. let height = block.get_height() as i32; if height == self.last_scanned_height() { // If the last scanned block is rescanned, check it still matches. if let Some(hash) = self.blocks.read().unwrap().last().map(|block| block.hash) { if block.hash() != hash { warn!("Likely reorg. Block hash does not match for block {}. {} vs {}", height, block.hash(), hash); return Err(height); } } return Ok(vec![]); } else if height != (self.last_scanned_height() + 1) { error!( "Block is not height-sequential (expected {}, found {})", self.last_scanned_height() + 1, height ); return Err(self.last_scanned_height()); } // Check to see that the previous block hash matches if let Some(hash) = self.blocks.read().unwrap().last().map(|block| block.hash) { if block.prev_hash() != hash { warn!("Likely reorg. Prev block hash does not match for block {}. {} vs {}", height, block.prev_hash(), hash); return Err(height-1); } } // Get the most recent scanned data. let mut block_data = BlockData { height, hash: block.hash(), tree: self .blocks .read() .unwrap() .last() .map(|block| block.tree.clone()) .unwrap_or(CommitmentTree::new()), }; // These are filled in inside the block let new_txs; let nfs: Vec<_>; { // Create a write lock let mut txs = self.txs.write().unwrap(); // Create a Vec containing all unspent nullifiers. // Include only the confirmed spent nullifiers, since unconfirmed ones still need to be included // during scan_block below. nfs = txs .iter() .map(|(txid, tx)| { let txid = *txid; tx.notes.iter().filter_map(move |nd| { if nd.spent.is_none() { Some((nd.nullifier, nd.account, txid)) } else { None } }) }) .flatten() .collect(); // Prepare the note witnesses for updating for tx in txs.values_mut() { for nd in tx.notes.iter_mut() { // Duplicate the most recent witness if let Some(witness) = nd.witnesses.last() { let clone = witness.clone(); nd.witnesses.push(clone); } // Trim the oldest witnesses nd.witnesses = nd .witnesses .split_off(nd.witnesses.len().saturating_sub(100)); } } new_txs = { let nf_refs = nfs.iter().map(|(nf, account, _)| (nf.to_vec(), *account)).collect::>(); let extfvks: Vec = self.zkeys.read().unwrap().iter().map(|zk| zk.extfvk.clone()).collect(); // Create a single mutable slice of all the newly-added witnesses. let mut witness_refs: Vec<_> = txs .values_mut() .map(|tx| tx.notes.iter_mut().filter_map( |nd| if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { nd.witnesses.last_mut() } else { None })) .flatten() .collect(); self.scan_block_internal( block.clone(), &extfvks, nf_refs, &mut block_data.tree, &mut witness_refs[..], pool, ) }; } // If this block had any new Txs, return the list of ALL txids in this block, // so the wallet can fetch them all as a decoy. let all_txs = if !new_txs.is_empty() { block.vtx.iter().map(|vtx| { let mut t = [0u8; 32]; t.copy_from_slice(&vtx.hash[..]); TxId{0: t} }).collect::>() } else { vec![] }; for tx in new_txs { // Create a write lock let mut txs = self.txs.write().unwrap(); // Mark notes as spent. let mut total_shielded_value_spent: u64 = 0; //info!("Txid {} belongs to wallet", tx.txid); for spend in &tx.shielded_spends { let txid = nfs .iter() .find(|(nf, _, _)| &nf[..] == &spend.nf[..]) .unwrap() .2; let mut spent_note = txs .get_mut(&txid) .unwrap() .notes .iter_mut() .find(|nd| &nd.nullifier[..] == &spend.nf[..]) .unwrap(); // Mark the note as spent, and remove the unconfirmed part of it info!("Marked a note as spent"); spent_note.spent = Some(tx.txid); spent_note.unconfirmed_spent = None::; total_shielded_value_spent += spent_note.note.value; } // Find the existing transaction entry, or create a new one. if !txs.contains_key(&tx.txid) { let tx_entry = WalletTx::new(block_data.height as i32, block.time as u64, &tx.txid); txs.insert(tx.txid, tx_entry); } let tx_entry = txs.get_mut(&tx.txid).unwrap(); tx_entry.total_shielded_value_spent = total_shielded_value_spent; // Save notes. for output in tx.shielded_outputs { let new_note = SaplingNoteData::new(&self.zkeys.read().unwrap()[output.account].extfvk, output); match LightWallet::note_address(self.config.hrp_sapling_address(), &new_note) { Some(a) => { // info!("Received sapling output to {}", a); self.ensure_hd_zaddresses(&a); }, None => {} } match tx_entry.notes.iter().find(|nd| nd.nullifier == new_note.nullifier) { None => tx_entry.notes.push(new_note), Some(_) => warn!("Tried to insert duplicate note for Tx {}", tx.txid) }; } } { let mut blks = self.blocks.write().unwrap(); // Store scanned data for this block. blks.push(block_data); // Trim the old blocks, keeping only as many as needed for a worst-case reorg (i.e. 101 blocks) let len = blks.len(); if len > MAX_REORG + 1 { let drain_first = len - (MAX_REORG+1); blks.drain(..drain_first); } } { // Cleanup mempool tx after adding a block, to remove all txns that got mined self.cleanup_mempool(); } // Print info about the block every 10,000 blocks if height % 10_000 == 0 { match self.get_sapling_tree() { Ok((h, hash, stree)) => info!("Sapling tree at height\n({}, \"{}\",\"{}\"),", h, hash, stree), Err(e) => error!("Couldn't determine sapling tree: {}", e) } } Ok(all_txs) } pub fn send_to_address ( &self, consensus_branch_id: u32, spend_params: &[u8], output_params: &[u8], _transparent_only: bool, tos: Vec<(&str, u64, Option)>, broadcast_fn: F ) -> Result<(String, Vec), String> where F: Fn(Box<[u8]>) -> Result { if !self.unlocked { return Err("Cannot spend while wallet is locked".to_string()); } let start_time = now(); if tos.len() == 0 { return Err("Need at least one destination address".to_string()); } // Check for duplicates in the to list - We need that for HushChat // 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::() as u64; println!( "0: Creating transaction sending {} puposhis to {} addresses", total_value, tos.len() ); // Convert address (str) to RecepientAddress and value to Amount let recepients = tos.iter().map(|to| { let ra = match address::RecipientAddress::from_str(to.0, self.config.hrp_sapling_address(), self.config.base58_pubkey_address(), self.config.base58_script_address()) { Some(to) => to, None => { let e = format!("Invalid recipient address: '{}'", to.0); error!("{}", e); return Err(e); } }; let value = Amount::from_u64(to.1).unwrap(); Ok((ra, value, to.2.clone())) }).collect::)>, String>>()?; // Target the next block, assuming we are up-to-date. let (height, anchor_offset) = match self.get_target_height_and_anchor_offset() { Some(res) => res, None => { let e = format!("Cannot send funds before scanning any blocks"); error!("{}", e); return Err(e); } }; // Select notes to cover the target value println!("{}: Selecting notes", now() - start_time); let target_value = Amount::from_u64(total_value).unwrap() + DEFAULT_FEE ; // Select the candidate notes that are eligible to be spent let notes: Vec<_> = self.txs.read().unwrap().iter() .map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note))) .flatten() .filter_map(|(txid, note)| { // Filter out notes that are already spent if note.spent.is_some() || note.unconfirmed_spent.is_some() { None } else { // Get the spending key for the selected fvk, if we have it let extsk = self.zkeys.read().unwrap().iter() .find(|zk| zk.extfvk == note.extfvk) .and_then(|zk| zk.extsk.clone()); SpendableNote::from(txid, note, anchor_offset, &extsk) } }) .scan(0, |running_total, spendable| { let value = spendable.note.value; let ret = if *running_total < u64::from(target_value) { Some(spendable) } else { None }; *running_total = *running_total + value; ret }) .collect(); let mut builder = Builder::new(height); // A note on t addresses // Funds received by t-addresses can't be explicitly spent in silentdragonlite. // silentdragonlite will lazily consolidate all t address funds into your shielded addresses. // Specifically, if you send an outgoing transaction that is sent to a shielded address, // silentdragonlite will add all your t-address funds into that transaction, and send them to your shielded // address as change. let tinputs: Vec<_> = self.get_utxos().iter() .filter(|utxo| utxo.unconfirmed_spent.is_none()) // Remove any unconfirmed spends .map(|utxo| utxo.clone()) .collect(); // Create a map from address -> sk for all taddrs, so we can spend from the // right address let address_to_sk = self.tkeys.read().unwrap().iter() .filter(|wtk| wtk.tkey.is_some()) .map(|wtk| (wtk.address.clone(), wtk.tkey.unwrap().clone())) .collect::>(); // Add all tinputs tinputs.iter() .map(|utxo| { let outpoint: OutPoint = utxo.to_outpoint(); let coin = TxOut { value: Amount::from_u64(utxo.value).unwrap(), script_pubkey: Script { 0: utxo.script.clone() }, }; if let Some(sk) = address_to_sk.get(&utxo.address) { return builder.add_transparent_input(*sk, outpoint.clone(), coin.clone()) } else { info!("Not adding a UTXO because secret key is absent."); return Ok(()) } }) .collect::, _>>() .map_err(|e| format!("{}", e))?; // Confirm we were able to select sufficient value let selected_value = notes.iter().map(|selected| selected.note.value).sum::() + tinputs.iter().map::(|utxo| utxo.value.into()).sum::(); if selected_value < u64::from(target_value) { let e = format!( "Insufficient verified funds (have {}, need {:?}). NOTE: funds need {} confirmations before they can be spent.", selected_value, target_value, self.config.anchor_offset ); error!("{}", e); return Err(e); } // Create the transaction println!("{}: Adding {} notes and {} utxos", now() - start_time, notes.len(), tinputs.len()); for selected in notes.iter() { if let Err(e) = builder.add_sapling_spend( selected.extsk.clone(), selected.diversifier, selected.note.clone(), selected.witness.clone(), ) { let e = format!("Error adding note: {:?}", e); error!("{}", e); return Err(e); } } // If no Sapling notes were added, add the change address manually. That is, // send the change to our sapling address manually. Note that if a sapling note was spent, // the builder will automatically send change to that address if notes.len() == 0 { builder.send_change_to( self.zkeys.read().unwrap()[0].extfvk.fvk.ovk, self.zkeys.read().unwrap()[0].zaddress.clone()); } // TODO: We're using the first ovk to encrypt outgoing Txns. Is that Ok? let ovk = self.zkeys.read().unwrap()[0].extfvk.fvk.ovk; for (to, value, memo) in recepients { // Compute memo if it exists let encoded_memo = match memo { None => None, Some(s) => match Memo::from_str(&s) { None => { let e = format!("Error creating output. Memo {:?} is too long", s); error!("{}", e); return Err(e); }, Some(m) => Some(m) } }; println!("{}: Adding output", now() - start_time); if let Err(e) = match to { address::RecipientAddress::Shielded(to) => { builder.add_sapling_output(ovk, to.clone(), value, encoded_memo) } address::RecipientAddress::Transparent(to) => { builder.add_transparent_output(&to, value) } } { let e = format!("Error adding output: {:?}", e); error!("{}", e); return Err(e); } } println!("{}: Building transaction", now() - start_time); let (tx, _) = match builder.build( consensus_branch_id, prover::InMemTxProver::new(spend_params, output_params), ) { Ok(res) => res, Err(e) => { let e = format!("Error creating transaction: {:?}", e); error!("{}", e); return Err(e); } }; println!("{}: Transaction created", now() - start_time); println!("Transaction ID: {}", tx.txid()); // Create the TX bytes let mut raw_tx = vec![]; tx.write(&mut raw_tx).unwrap(); let txid = broadcast_fn(raw_tx.clone().into_boxed_slice())?; // Mark notes as spent. { // Mark sapling notes as unconfirmed spent let mut txs = self.txs.write().unwrap(); for selected in notes { let mut spent_note = txs.get_mut(&selected.txid).unwrap() .notes.iter_mut() .find(|nd| &nd.nullifier[..] == &selected.nullifier[..]) .unwrap(); spent_note.unconfirmed_spent = Some(tx.txid()); } // Mark this utxo as unconfirmed spent for utxo in tinputs { let mut spent_utxo = txs.get_mut(&utxo.txid).unwrap().utxos.iter_mut() .find(|u| utxo.txid == u.txid && utxo.output_index == u.output_index) .unwrap(); spent_utxo.unconfirmed_spent = Some(tx.txid()); } } // Add this Tx to the mempool structure { let mut mempool_txs = self.mempool_txs.write().unwrap(); match mempool_txs.get_mut(&tx.txid()) { None => { // Collect the outgoing metadata let outgoing_metadata = tos.iter().map(|(addr, amt, maybe_memo)| { OutgoingTxMetadata { address: addr.to_string(), value: *amt, memo: match maybe_memo { None => Memo::default(), Some(s) => { // If the address is not a z-address, then drop the memo if LightWallet::is_shielded_address(&addr.to_string(), &self.config) { Memo::from_str(s).unwrap() } else { Memo::default() } } }, } }).collect::>(); // Create a new WalletTx let mut wtx = WalletTx::new(height as i32, now() as u64, &tx.txid()); wtx.outgoing_metadata = outgoing_metadata; // Add it into the mempool mempool_txs.insert(tx.txid(), wtx); }, Some(_) => { warn!("A newly created Tx was already in the mempool! How's that possible? Txid: {}", tx.txid()); } } } Ok((txid, raw_tx)) } // After some blocks have been mined, we need to remove the Txns from the mempool_tx structure // if they : // 1. Have expired // 2. The Tx has been added to the wallet via a mined block pub fn cleanup_mempool(&self) { const DEFAULT_TX_EXPIRY_DELTA: i32 = 20; let current_height = self.blocks.read().unwrap().last().map(|b| b.height).unwrap_or(0); { // Remove all expired Txns self.mempool_txs.write().unwrap().retain( | _, wtx| { current_height < (wtx.block + DEFAULT_TX_EXPIRY_DELTA) }); } { // Remove all txns where the txid is added to the wallet directly self.mempool_txs.write().unwrap().retain ( |txid, _| { self.txs.read().unwrap().get(txid).is_none() }); } } } #[cfg(test)] pub mod tests;