// 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 sodiumoxide::crypto::pwhash::argon2id13; 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}, 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 note_selection; mod extended_key; mod utils; mod address; mod prover; mod walletzkey; use data::{BlockData, WalletTx, Utxo, SaplingNoteData, SpendableNote, OutgoingTxMetadata, IncomingTxMetadata}; use extended_key::{KeyIndex, ExtendedPrivKey}; use walletzkey::{WalletZKey, WalletTKey, WalletZKeyType}; use note_selection::{NoteCand, SelectError, select_spends, max_spends_for_size, format_drgx, MAX_SPENDS_PER_TX}; 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() } } /// Which notes a z-balance sum counts. `Addr` compares (diversifier, pk_d) directly. Because /// encode_payment_address is injective on (diversifier, pk_d), it selects exactly the notes whose /// old derived string == encode(pa). `Str` keeps the old exact string compare (on the cheap /// note_address) for arbitrary caller strings, so no decoding and no new panic paths. enum ZAddrFilter { All, Addr(PaymentAddress), Str(String), } impl ZAddrFilter { fn from_opt(addr: &Option) -> Self { match addr { Some(a) => ZAddrFilter::Str(a.clone()), None => ZAddrFilter::All, } } fn matches(&self, hrp: &str, nd: &SaplingNoteData) -> bool { match self { ZAddrFilter::All => true, ZAddrFilter::Addr(pa) => nd.diversifier == pa.diversifier && nd.note.pk_d == pa.pk_d, ZAddrFilter::Str(a) => LightWallet::note_address(hrp, nd).as_ref() == Some(a), } } } 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. // C253: passphrase KDF descriptor (non-secret; all zero unless kdf == 1 and encrypted). kdf: u8, kdf_salt: [u8; 16], kdf_ops: u32, kdf_mem: u32, 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>>, pub incoming_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; } /// C253: the version written ONLY for a wallet actually protected by the salted Argon2id KDF. pub const ARGON2_WALLET_VERSION: u64 = 10; /// Highest wallet version this build can read. pub fn max_read_version() -> u64 { LightWallet::ARGON2_WALLET_VERSION } /// C253: derive the secretbox key from the passphrase. kdf 0 = legacy unsalted double SHA-256; kdf 1 = Argon2id13 with the stored salt/ops/mem. pub(crate) fn derive_wallet_key(kdf: u8, passwd: &[u8], salt: &[u8; 16], ops: u32, mem: u32) -> io::Result { if kdf == 0 { return secretbox::Key::from_slice(&double_sha256(passwd)) .ok_or_else(|| io::Error::new(ErrorKind::InvalidData, "bad key length")); } if ops < 1 || ops > 12 || mem < (8u32 << 20) || mem > (1u32 << 30) { return Err(io::Error::new(ErrorKind::InvalidData, "Wallet key-derivation parameters are corrupt")); } let _ = sodiumoxide::init(); let mut kb = [0u8; secretbox::KEYBYTES]; let r = argon2id13::derive_key(&mut kb, passwd, &argon2id13::Salt(*salt), argon2id13::OpsLimit(ops as usize), argon2id13::MemLimit(mem as usize)); let key = match r { Ok(_) => secretbox::Key::from_slice(&kb), Err(_) => None, }; sodiumoxide::utils::memzero(&mut kb[..]); key.ok_or_else(|| io::Error::new(ErrorKind::Other, "Argon2id key derivation failed")) } 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![], kdf: if std::env::var("DRAGONX_LITE_KDF").map(|v| v == "legacy").unwrap_or(false) { 0 } else { 1 }, kdf_salt: [0u8; 16], kdf_ops: 0, kdf_mem: 0, 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())), incoming_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::max_read_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::Unsupported, 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)?) }; 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![] }; // C253: version 10 carries the passphrase KDF descriptor between the nonce and the seed. v<=9 is always legacy double SHA-256. let mut kdf = 0u8; let mut kdf_salt = [0u8; 16]; let mut kdf_ops = 0u32; let mut kdf_mem = 0u32; if version >= LightWallet::ARGON2_WALLET_VERSION { kdf = reader.read_u8()?; reader.read_exact(&mut kdf_salt)?; kdf_ops = reader.read_u32::()?; kdf_mem = reader.read_u32::()?; if kdf != 1 || !encrypted || kdf_ops < 1 || kdf_ops > 12 || kdf_mem < (8u32 << 20) || kdf_mem > (1u32 << 30) { return Err(io::Error::new(ErrorKind::InvalidData, "Wallet key-derivation data is corrupt")); } } // 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)?)) })?; 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::()?; let wallet = LightWallet{ encrypted: encrypted, unlocked: !encrypted, // When reading from disk, if wallet is encrypted, it starts off locked. enc_seed: enc_seed, nonce: nonce, kdf, kdf_salt, kdf_ops, kdf_mem, 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())), incoming_mempool_txs: Arc::new(RwLock::new(HashMap::new())), config: config.clone(), birthday, total_scan_duration: Arc::new(RwLock::new(vec![Duration::new(0, 0)])), }; // Existing wallets carry up to 100 witnesses per long-spent note; drop them now so the next // save is small. Same predicate as the per-block call in scan_block_with_pool. let cleared = wallet.clear_old_witnesses(); if cleared > 0 { info!("Dropped witness history of {} notes whose spend is deeper than {} blocks", cleared, MAX_REORG); } Ok(wallet) } 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 // C253: v10 only for a wallet actually protected by the Argon2id KDF; everything else stays v9 (readable by older builds and SDXL). let argon = self.encrypted && self.kdf == 1; writer.write_u64::(if argon { LightWallet::ARGON2_WALLET_VERSION } else { 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))?; if argon { writer.write_u8(1)?; writer.write_all(&self.kdf_salt)?; writer.write_u32::(self.kdf_ops)?; writer.write_u32::(self.kdf_mem)?; } // 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 { // note.pk_d == g_d(diversifier) * ivk(note.extfvk) by construction: decryption // (parse_note_plaintext_without_memo) and SaplingNoteData::read (data.rs:158) both build // it that way. So this encodes exactly what into_payment_address() would, without the // group hash, ivk hash and 252-bit scalar multiplication. let pa = PaymentAddress { pk_d: note.note.pk_d.clone(), diversifier: note.diversifier }; Some(encode_payment_address(hrp, &pa)) } 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(); } // PBKDF2 (the slow part) outside the lock; hold ONE write guard across read-modify-write. let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), ""); let mut tkeys = self.tkeys.write().unwrap(); let mut pos = tkeys.len() as u32; // same start index as today (counts imported keys too - unchanged) loop { let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos); let address = self.address_from_sk(&sk); if tkeys.iter().any(|t| t.address == address) { pos += 1; continue; } tkeys.push(WalletTKey::new_hdkey(sk, address.clone())); return 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(); } // An imported key would be stored with no enc_key; lock() could then never succeed (see lock()). if self.encrypted { return "Error: Can't import a private key while the wallet is encrypted - remove encryption, import, then encrypt again".to_string(); } //// Decode the WIF: Base58Check (checksum verified), version byte, 32-byte key, 0x01 compressed flag. //// Never panics, and never echoes the key in an error. let sk_raw = match LightWallet::decode_taddr_wif(&sk, self.config.base58_secretkey_prefix()[0]) { Ok(k) => k, Err(e) => return format!("Error: {}", e), }; //// 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(); } // An imported key would be stored with no enc_key; lock() could then never succeed (see lock()). if self.encrypted { return "Error: Can't import a private key while the wallet is encrypted - remove encryption, import, then encrypt again".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) { match self.blocks.write() { Ok(mut b) => b.clear(), Err(p) => p.into_inner().clear(), }; match self.txs.write() { Ok(mut t) => t.clear(), Err(p) => p.into_inner().clear(), }; match self.mempool_txs.write() { Ok(mut m) => m.clear(), Err(p) => p.into_inner().clear(), }; match self.incoming_mempool_txs.write() { Ok(mut m) => m.clear(), Err(p) => p.into_inner().clear(), }; } pub fn set_initial_block(&self, height: i32, hash: &str, sapling_tree: &str) -> bool { let mut blocks = match self.blocks.write() { Ok(b) => b, Err(p) => p.into_inner(), }; 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, &[]) } /// WIF = version(1) || key(32) || 0x01 (compressed). `bs58 ... with_check(None)` verifies the double-SHA256 /// checksum and strips it (bs58 0.2.5 decode.rs:284-310), keeping the version byte. Do NOT pass /// `with_check(Some(v))`: it would pin one version and reject the 0x80 (K/L) WIFs the help text advertises. /// Accepts the config prefix (0xBC on main) and 0x80. The decoded bytes are wiped on every path (s2-D5), /// and no error contains the key. pub(crate) fn decode_taddr_wif(wif: &str, network_prefix: u8) -> Result { let wif = wif.trim(); // Decode into a buffer we own (bs58's into_vec allocates the same size), so it is wiped even when the // checksum fails; into_vec would drop its buffer, which then holds the decoded bytes, unwiped. let mut buf = vec![0u8; wif.len()]; let r = match bs58::decode(wif).with_check(None).into(&mut buf[..]) { Err(_) => Err("Couldn't decode the transparent private key (not valid Base58Check)".to_string()), Ok(33) => Err("Uncompressed transparent private keys are not supported; import the compressed form".to_string()), Ok(n) if n != 34 || buf[33] != 0x01 => Err("Couldn't decode the transparent private key (unrecognized format)".to_string()), Ok(_) if buf[0] != network_prefix && buf[0] != 0x80 => Err("The transparent private key is for a different network".to_string()), Ok(_) => SecretKey::from_slice(&buf[1..33]).map_err(|_| "Invalid transparent private key".to_string()), }; sodiumoxide::utils::memzero(&mut buf[..]); // already a dependency; no new crate r } 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 { // An encrypted wallet with a zero seed is locked, or was left half-locked by an older build: never // return the all-zero-entropy phrase ("abandon ... art") as if it were the backup. if !self.unlocked || (self.encrypted && self.seed == [0u8; 32]) { 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")); } // C253: legacy wallets keep the unsalted double SHA-256; new wallets use salted Argon2id (a fresh salt per encryption). let (salt, ops, mem) = if self.kdf == 1 { (argon2id13::gen_salt().0, argon2id13::OPSLIMIT_MODERATE.0 as u32, argon2id13::MEMLIMIT_MODERATE.0 as u32) } else { ([0u8; 16], 0u32, 0u32) }; let key = LightWallet::derive_wallet_key(self.kdf, passwd.as_bytes(), &salt, ops, mem)?; 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(); self.kdf_salt = salt; self.kdf_ops = ops; self.kdf_mem = mem; // 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")); } // Check every key before changing anything. A key with no encrypted copy can't be locked, and // failing halfway used to leave the seed zeroed while the wallet still reported unlocked. let t_ok = self.tkeys.read().unwrap().iter().all(|tk| tk.can_lock()); let z_ok = self.zkeys.read().unwrap().iter().all(|zk| zk.can_lock()); if !t_ok || !z_ok { return Err(io::Error::new(ErrorKind::InvalidInput, "Can't lock: the wallet holds a private key that is not encrypted")); } // 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::>>()?; // Empty the seed last self.seed.copy_from_slice(&[0u8; 32]); 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 = LightWallet::derive_wallet_key(self.kdf, passwd.as_bytes(), &self.kdf_salt, self.kdf_ops, self.kdf_mem)?; let nonce = match secretbox::Nonce::from_slice(&self.nonce) { Some(n) => n, None => return Err(io::Error::new(ErrorKind::InvalidData, "Wallet encryption data is corrupt")), }; 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 let t = self.tkeys.write().unwrap().iter_mut().map(|tk| { tk.unlock(&key) }).collect::>>(); // Go over the zkeys, and add the spending keys again let z = t.and_then(|_| self.zkeys.write().unwrap().iter_mut().map(|zk| { zk.unlock(&self.config, bip39_seed.as_bytes(), &key) }).collect::>>()); // A key failed (a zaddress/fvk mismatch, corrupt encryption data): wipe the keys decrypted so far // before returning. The wallet stays locked, and write() saves a locked wallet, so they would // otherwise reach the encrypted file in plaintext. if let Err(e) = z { for tk in self.tkeys.write().unwrap().iter_mut() { if tk.can_lock() { let _ = tk.lock(); } } for zk in self.zkeys.write().unwrap().iter_mut() { if zk.can_lock() { let _ = zk.lock(); } } return Err(e); } // 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")); } // Always prove the passphrase, also when the wallet is already unlocked: from the next save on, the // seed and every key are written in plaintext. { let key = LightWallet::derive_wallet_key(self.kdf, passwd.as_bytes(), &self.kdf_salt, self.kdf_ops, self.kdf_mem)?; let nonce = match secretbox::Nonce::from_slice(&self.nonce) { Some(n) => n, None => return Err(io::Error::new(ErrorKind::InvalidData, "Wallet encryption data is corrupt")), }; if secretbox::open(&self.enc_seed, &nonce, &key).is_err() { return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?")); } } // 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]); self.kdf_salt = [0u8; 16]; self.kdf_ops = 0; self.kdf_mem = 0; 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 { let unconfirmed_balance = self.unconfirmed_zbalance(addr.clone()); let confirmed_balance = self.confirmed_zbalance_f(&ZAddrFilter::from_opt(&addr)); confirmed_balance + unconfirmed_balance } // Unspent (and not pending-spent) note value matching `f`. The spent checks run first so spent // notes never reach the address filter; both are pure, so the sum equals the old // filter(addr).map(if unspent {v} else {0}).sum(). fn confirmed_zbalance_f(&self, f: &ZAddrFilter) -> u64 { let hrp = self.config.hrp_sapling_address(); self.txs.read().unwrap() .values() .map(|tx| { tx.notes.iter() .filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none()) .filter(|nd| f.matches(hrp, nd)) .map(|nd| nd.note.value) .sum::() }) .sum::() } // 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 { self.verified_zbalance_f(&ZAddrFilter::from_opt(&addr)) } fn verified_zbalance_f(&self, f: &ZAddrFilter) -> u64 { let anchor_height = match self.get_target_height_and_anchor_offset() { Some((height, anchor_offset)) => height - anchor_offset as u32 , None => return 0, }; let hrp = self.config.hrp_sapling_address(); 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| f.matches(hrp, nd)) .map(|nd| nd.note.value) .sum::() } else { 0 } }) .sum::() } pub fn spendable_zbalance(&self, addr: Option) -> u64 { self.spendable_zbalance_f(&ZAddrFilter::from_opt(&addr)) } // Lock order: txs.read, then zkeys.read inside have_spendingkey_for_extfvk (same order as // scan_block). Never call this while holding a zkeys guard. fn spendable_zbalance_f(&self, f: &ZAddrFilter) -> u64 { let anchor_height = self.get_anchor_height(); let hrp = self.config.hrp_sapling_address(); 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| f.matches(hrp, nd)) .filter(|nd| { // Check to see if we have this note's spending key. self.have_spendingkey_for_extfvk(&nd.extfvk) }) .map(|nd| nd.note.value) .sum::() } else { 0 } }) .sum::() as u64 } pub fn unconfirmed_zbalance(&self, addr: Option) -> u64 { self.incoming_mempool_txs.read().unwrap() .values() .flat_map(|txs| txs.iter()) .map(|tx| { tx.incoming_metadata.iter() .filter(|meta| { match addr.as_ref() { Some(a) => { a == &meta.address }, None => true } }) .map(|meta| { meta.value }) .sum::() }) .sum::() } pub fn have_spendingkey_for_extfvk(&self, extfvk: &ExtendedFullViewingKey) -> bool { match self.zkeys.read().unwrap().iter().find(|zk| zk.extfvk == *extfvk) { None => false, Some(zk) => zk.have_spending_key() } } pub fn have_spending_key_for_zaddress(&self, address: &String) -> bool { match self.zkeys.read().unwrap().iter() .find(|zk| encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress) == *address) { None => false, Some(zk) => zk.have_spending_key() } } /// Per-zkey z balances for do_balance, in zkeys order: /// (address, zbalance, unconfirmed, verified_zbalance, spendable_zbalance). /// Equal to calling zbalance/unconfirmed_zbalance/verified_zbalance/spendable_zbalance with /// Some(encoded zaddress), in that order, without re-deriving any note's address. pub fn z_balances_by_address(&self) -> Vec<(String, u64, u64, u64, u64)> { let hrp = self.config.hrp_sapling_address(); // Snapshot, then RELEASE the zkeys guard before any txs lock: scan_block holds txs.write // while taking zkeys.read/write (ensure_hd_zaddresses -> add_zaddr), and // spendable_zbalance_f re-reads zkeys. Holding zkeys here would invert the lock order // and re-enter a writer-preferring RwLock (deadlock). let addrs: Vec<(String, PaymentAddress)> = self.zkeys.read().unwrap().iter() .map(|zk| (encode_payment_address(hrp, &zk.zaddress), zk.zaddress.clone())) .collect(); addrs.into_iter().map(|(address, pa)| { let f = ZAddrFilter::Addr(pa); let zbalance = { let unconfirmed_balance = self.unconfirmed_zbalance(Some(address.clone())); self.confirmed_zbalance_f(&f) + unconfirmed_balance }; let unconfirmed = self.unconfirmed_zbalance(Some(address.clone())); let verified = self.verified_zbalance_f(&f); let spendable = self.spendable_zbalance_f(&f); (address, zbalance, unconfirmed, verified, spendable) }).collect() } /// Encoded z-addresses whose FIRST zkey with that address has a spending key. Same set as /// get_all_zaddresses() filtered by have_spending_key_for_zaddress (Z encodes, not Z + Z^2). pub fn spendable_zaddress_set(&self) -> HashSet { let hrp = self.config.hrp_sapling_address(); let mut first: HashMap = HashMap::new(); for zk in self.zkeys.read().unwrap().iter() { first.entry(encode_payment_address(hrp, &zk.zaddress)) .or_insert_with(|| zk.have_spending_key()); } first.into_iter().filter(|(_, spendable)| *spendable).map(|(a, _)| a).collect() } 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() { // The epk is chain data. Sapling consensus rejects only a small-order epk, so one outside the prime-order // subgroup can be mined; it is never ours (the compact scan skips it too). Skip the output, never panic. let epk_prime = match output.ephemeral_key.as_prime_order(&JUBJUB) { Some(p) => p, None => { warn!("Output in tx {} has a non-prime-order epk; skipping it", tx.txid()); continue; } }; 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 (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 // Collect IVKs to detect change outputs to diversified addresses let ivks_for_change: Vec<_> = self.zkeys.read().unwrap().iter() .map(|zk| zk.extfvk.fvk.vk.ivk()) .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, &epk_prime, &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 output belongs to our wallet using IVK decryption. // This correctly detects change sent to diversified addresses, // not just the wallet's default z-address. let is_to_self = ivks_for_change.iter().any(|ivk| { try_sapling_note_decryption(ivk, &epk_prime, &output.cmu, &output.enc_ciphertext).is_some() }); // If this is change (funds to ourself) without a meaningful memo, skip it. // memo.to_utf8() returns Some(Ok("")) for empty memos (all-zero bytes), // so we must also check for empty strings, not just None. if is_to_self { let has_memo = match memo.to_utf8() { Some(Ok(ref s)) if !s.is_empty() => true, _ => false, }; if !has_memo { continue; } } // Update the WalletTx // Do it in a short scope because of the write lock. { info!("A sapling output was sent in {}", tx.txid()); match self.txs.write() { Ok(mut txs) => { match txs.get(&tx.txid()) { Some(wtx) => { if wtx.outgoing_metadata.iter() .any(|om| om.address == address && om.value == note.value && om.memo == memo) { 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 => { error!("Can not find any entry for txid : {}", tx.txid()); continue; } } }, Err(poisoned) => { error!("Lock is poisoned: {}", poisoned); return; } } } }, 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 => {}, }; } } pub fn scan_full_mempool_tx(&self, tx: &Transaction, height: i32, _datetime: u64, mempool_transaction: bool) { if tx.shielded_outputs.is_empty() { error!("Something went wrong, there are no shielded outputs"); return; } for output in tx.shielded_outputs.iter() { // As in scan_full_tx: an epk outside the prime-order subgroup is never ours. Skip the output, never panic. let epk_prime = match output.ephemeral_key.as_prime_order(&JUBJUB) { Some(p) => p, None => { warn!("Mempool tx {} has an output with a non-prime-order epk; skipping it", tx.txid()); continue; } }; 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 (note, _to, memo) = match try_sapling_note_decryption(&ivk, &epk_prime, &cmu, &ct) { Some(ret) => ret, None => continue, }; if mempool_transaction { let mut incoming_mempool_txs = match self.incoming_mempool_txs.write() { Ok(txs) => txs, Err(e) => { error!("Error acquiring write lock: {}", e); return; } }; let addr = encode_payment_address(self.config.hrp_sapling_address(), &_to); let amt = note.value; let mut wtx = WalletTx::new(height, now() as u64, &tx.txid()); let formatted_memo = LightWallet::memo_str(&Some(memo.clone())); let existing_txs = incoming_mempool_txs.entry(tx.txid()) .or_insert_with(Vec::new); if formatted_memo.as_ref().map_or(false, |m| !m.is_empty()) { // Check if a transaction with the exact same memo already exists if existing_txs.iter().any(|tx| tx.incoming_metadata.iter().any(|meta| LightWallet::memo_str(&Some(meta.memo.clone())) == formatted_memo.as_ref().cloned())) { // Transaction with this memo already exists, do nothing return; } let incoming_metadata = IncomingTxMetadata { address: addr.clone(), value: amt, memo: memo.clone(), incoming_mempool: true, }; wtx.incoming_metadata.push(incoming_metadata); existing_txs.push(wtx); let mut txs = match self.txs.write() { Ok(t) => t, Err(e) => { error!("Error acquiring write lock: {}", e); return; } }; if let Some(wtx) = txs.get_mut(&tx.txid()) { wtx.incoming_metadata.push(IncomingTxMetadata { address: addr.clone(), value: amt, memo: memo.clone(), incoming_mempool: true, }); } else { let mut new_wtx = WalletTx::new(height, now() as u64, &tx.txid()); new_wtx.incoming_metadata.push(IncomingTxMetadata { address: addr.clone(), value: amt, memo: memo.clone(), incoming_mempool: true, }); txs.insert(tx.txid(), new_wtx); } info!("Successfully added txid with memo"); } else { // Check if txid already exists in the hashmap let txid_exists = match self.txs.read() { Ok(t) => t.contains_key(&tx.txid()), Err(e) => { error!("Error acquiring read lock: {}", e); return; } }; if txid_exists { // If txid already exists, do not process further info!("Txid already exists, not adding"); return; } let incoming_metadata = IncomingTxMetadata { address: addr.clone(), value: amt, memo: memo.clone(), incoming_mempool: true, }; wtx.incoming_metadata.push(incoming_metadata); existing_txs.push(wtx); let mut txs = match self.txs.write() { Ok(t) => t, Err(e) => { error!("Error acquiring write lock: {}", e); return; } }; if let Some(wtx) = txs.get_mut(&tx.txid()) { wtx.incoming_metadata.push(IncomingTxMetadata { address: addr.clone(), value: amt, memo: memo.clone(), incoming_mempool: true, }); } else { let mut new_wtx = WalletTx::new(height, now() as u64, &tx.txid()); new_wtx.incoming_metadata.push(IncomingTxMetadata { address: addr.clone(), value: amt, memo: memo.clone(), incoming_mempool: true, }); txs.insert(tx.txid(), new_wtx); } info!("Successfully added txid"); } } else { info!("Not a mempool transaction"); } // 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 => {}, }; } } } } /// The height below which a confirmed spend can no longer be rolled back, or None while the block /// window is not full. When the window holds > MAX_REORG blocks, every retained block is at or above /// the returned height. An invalidate_block() that removes a spender whose WalletTx.block is below it /// therefore pops the whole window, and that already empties every note's witness list /// (each list holds <= MAX_REORG entries, and the window holds > MAX_REORG blocks). fn witness_clear_height(&self) -> Option { let blks = match self.blocks.read() { Ok(b) => b, Err(p) => p.into_inner(), }; if blks.len() > MAX_REORG { blks.first().map(|b| b.height) } else { None } } /// Drop the witness history of notes whose confirmed spender (keyed by WalletTx.block, the same /// field invalidate_block uses to decide removal) is below `clear_below`. Returns the number cleared. fn clear_spent_witnesses_below(txs: &mut HashMap, clear_below: i32) -> usize { let mut to_clear: Vec<(TxId, usize)> = Vec::new(); for (txid, tx) in txs.iter() { for (i, nd) in tx.notes.iter().enumerate() { if nd.witnesses.is_empty() { continue; } if let Some(spend_txid) = nd.spent { if let Some(spend_tx) = txs.get(&spend_txid) { if spend_tx.block < clear_below { to_clear.push((*txid, i)); } } } } } for (txid, i) in &to_clear { if let Some(tx) = txs.get_mut(txid) { // Vec::new(), not clear(): release the buffer. tx.notes[*i].witnesses = Vec::new(); } } to_clear.len() } /// Clear the history of long-spent notes (used right after loading a wallet). pub fn clear_old_witnesses(&self) -> usize { match self.witness_clear_height() { None => 0, Some(h) => { let mut txs = match self.txs.write() { Ok(t) => t, Err(p) => p.into_inner(), }; LightWallet::clear_spent_witnesses_below(&mut *txs, h) } } } // 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 = match self.blocks.write() { Ok(b) => b, Err(p) => p.into_inner(), }; while let Some(last) = blks.last() { if last.height < at_height { break; } blks.pop(); num_invalidated += 1; } } // Next, remove entire transactions { let mut txs = match self.txs.write() { Ok(t) => t, Err(p) => p.into_inner(), }; 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; } // unconfirmed_spent is left as it is: a pending send can still be mined on the // new branch. Only a block that spends the note (scan_block) or a reload clears // it, the same as for a send that expires unmined. }) }) } // Of the notes that still remain, unroll the witness. // Remove `num_invalidated` items from the witness { let mut txs = match self.txs.write() { Ok(t) => t, Err(p) => p.into_inner(), }; // 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 } // Below these sizes the old inline path is kept / threads are not worth their start-up cost. const PAR_WITNESS_MIN_NOTES: usize = 16; const PAR_WITNESS_MIN_APPENDS: usize = 128; /// Append `nodes` (in order) to every witness. Equivalent to /// `for w in witnesses { for n in nodes { w.append(*n).unwrap() } }`; large inputs are split over /// scoped threads, each owning a disjoint chunk of witnesses. fn append_nodes_to_witnesses( witnesses: &mut [&mut IncrementalWitness], nodes: &[Node], min_parallel_appends: usize, ) { let max_threads_by_work = witnesses.len() / 8; let total = witnesses.len().saturating_mul(nodes.len()); let threads = if max_threads_by_work < 2 || total < min_parallel_appends { 1 } else { let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1); cmp::min(cmp::min(cores, 8), max_threads_by_work) }; if threads < 2 { for w in witnesses.iter_mut() { for n in nodes { w.append(*n).expect("sapling tree full"); } } return; } let chunk_size = (witnesses.len() + threads - 1) / threads; std::thread::scope(|s| { for chunk in witnesses.chunks_mut(chunk_size) { s.spawn(move || { for w in chunk.iter_mut() { for n in nodes { w.append(*n).expect("sapling tree full"); } } }); } }); } /// Commit one leaf (an output's cmu): to the existing notes' witnesses (or the deferred list), to the witnesses of /// this block's and this tx's earlier outputs, and to the tree. Every output whose cmu parses is a leaf, whether /// or not it can be trial-decrypted; a skipped leaf makes every later anchor diverge from consensus. fn append_leaf(node: Node, tree: &mut CommitmentTree, defer_existing: bool, deferred_nodes: &mut Vec, existing_witnesses: &mut [&mut IncrementalWitness], block_witnesses: &mut [&mut IncrementalWitness], new_witnesses: &mut [&mut IncrementalWitness]) { if defer_existing { deferred_nodes.push(node); } else { for witness in existing_witnesses.iter_mut() { witness.append(node).unwrap(); } } for witness in block_witnesses.iter_mut() { witness.append(node).unwrap(); } for witness in new_witnesses.iter_mut() { witness.append(node).unwrap(); } tree.append(node).unwrap(); } /// 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], defer_existing: bool, deferred_nodes: &mut Vec, block_witnesses: &mut [&mut IncrementalWitness], new_witnesses: &mut [&mut IncrementalWitness], pool: &ThreadPool ) -> Option { // A cmu we cannot parse means we cannot append the correct leaf to the commitment tree, // so EVERY later anchor/witness silently diverges from consensus — this is the exact class // of bug behind "missing sapling anchor" after a long rescan. We can't reconstruct the leaf // here, but log it LOUDLY (was a silent `cmu().ok()?` early-return) so the divergence is // diagnosable; the practical cure is to reseed from a newer checkpoint past the bad block. let cmu = match output.cmu() { Ok(cmu) => cmu, Err(_) => { error!("Failed to parse cmu for output index {} — commitment tree will DIVERGE from consensus from this block onward; reseed from a newer checkpoint", index); return None; } }; // The note commitment (cmu) MUST be appended to the commitment tree for EVERY output, // regardless of whether we can decrypt it — the tree must match consensus or every later // anchor/witness diverges (silently). epk is needed only for trial-decryption, so an epk // parse failure must NOT skip the tree append (the old `epk().ok()?` early-return did, and // silently). On epk failure: log it, still commit cmu to the tree, then skip decryption. let epk = match output.epk() { Ok(epk) => epk, Err(_) => { error!("Failed to parse epk for output index {} — committing cmu to tree, skipping decryption", index); LightWallet::append_leaf(Node::new(cmu.into()), tree, defer_existing, deferred_nodes, existing_witnesses, block_witnesses, new_witnesses); return None; } }; // The server controls the ciphertext length, and try_sapling_compact_note_decryption asserts 52 bytes // (COMPACT_NOTE_SIZE). Longer: the compact plaintext is exactly the first 52 bytes (the cmu re-check validates // it), so truncate. Shorter: undecryptable; commit the cmu like any other leaf and skip decryption. let ct = output.ciphertext; if ct.len() < 52 { error!("Output index {} has a {}-byte compact ciphertext (expected 52) — committing cmu to tree, skipping decryption", index, ct.len()); LightWallet::append_leaf(Node::new(cmu.into()), tree, defer_existing, deferred_nodes, existing_witnesses, block_witnesses, new_witnesses); return None; } let ct = if ct.len() > 52 { ct[..52].to_vec() } else { ct }; 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 || { // Exactly one message per job: a panic is caught and sent as None, so the collector below never // waits for a result that cannot come. let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { try_sapling_compact_note_decryption(&ivk, &epk, &cmu, &ct) })); let msg = match r { Ok(Some((note, to))) => Some(Some((note, to, account))), Ok(None) => Some(None), Err(_) => None, }; if let Err(e) = tx.send(msg) { error!("Compact decryption result lost: {:?}", e); } }); }); // Only the jobs hold senders now: a job that ends without a message makes recv() below fail, not block. drop(tx); // Increment tree and witnesses LightWallet::append_leaf(Node::new(cmu.into()), tree, defer_existing, deferred_nodes, existing_witnesses, block_witnesses, new_witnesses); // 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 = match rx.recv() { Ok(n) => n, Err(_) => { error!("A compact decryption job for output index {} ended without a result", index); None } }; let epk = epk.clone(); let wso = match n { None => { error!("Compact decryption failed (panic) for output index {}; treating it as not ours", index); 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::>(); // Large wallets: collect this block's leaves and append them to the existing notes' witnesses // once, at the end, in output order (in parallel when worthwhile). Small wallets keep the inline path. let defer_existing = existing_witnesses.len() >= LightWallet::PAR_WITNESS_MIN_NOTES; let mut deferred_nodes: Vec = Vec::new(); 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, defer_existing, &mut deferred_nodes, &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, }); } } if !deferred_nodes.is_empty() { LightWallet::append_nodes_to_witnesses(existing_witnesses, &deferred_nodes, LightWallet::PAR_WITNESS_MIN_APPENDS); } 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); } }; // CompactBlock::hash()/prev_hash() assert 32 bytes when no header is sent, and every vtx hash is copied into a // [u8; 32] (one of them under txs.write()). Malformed server data is an error before any change, never a panic. // Err(-1) as for an unparsable block: Err(height) would refetch the same block forever under sync_lock. if (block.header().is_none() && (block.hash.len() != 32 || block.prevHash.len() != 32)) || block.vtx.iter().any(|t| t.hash.len() != 32) { error!("Malformed CompactBlock at height {} (bad hash length)", block.height); 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<_>; // Read before taking the txs lock (no lock nesting). The window ends at height-1 here. let witness_clear_below = self.witness_clear_height(); { // 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(); // Free the witness history of notes whose spend can no longer be rolled back. if let Some(h) = witness_clear_below { LightWallet::clear_spent_witnesses_below(&mut *txs, h); } // Prepare the note witnesses for updating for tx in txs.values_mut() { for nd in tx.notes.iter_mut() { // Duplicate the most recent witness (shares the allocation; deep-cloned only when appended to) if let Some(witness) = nd.witnesses.last() { let shared = Arc::clone(witness); nd.witnesses.push(shared); // Trim the oldest witnesses in place (no fresh Vec per note per block) let len = nd.witnesses.len(); if len > MAX_REORG { nd.witnesses.drain(..len - MAX_REORG); } } } } 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. // A block without shielded outputs appends nothing to any witness: don't un-share (deep-clone) them. // Advance every note whose nullifier is still tracked (spent.is_none(), the `nfs` set), pending // ones included: a reload, or a reorg of the block that mined the send, makes them spendable again. let block_has_outputs = block.vtx.iter().any(|t| !t.outputs.is_empty()); let mut witness_refs: Vec<&mut IncrementalWitness> = if block_has_outputs { txs .values_mut() .map(|tx| tx.notes.iter_mut().filter_map( |nd| if nd.spent.is_none() { nd.witnesses.last_mut().map(|w| Arc::make_mut(w)) } else { None })) .flatten() .collect() } else { Vec::new() }; 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 { // The note can be gone if the wallet was cleared or reorged after `nfs` was taken. Never panic // here (it would poison `txs`): skip the spend with a warning; the next rescan counts it. let spent_note = match nfs .iter() .find(|(nf, _, _)| &nf[..] == &spend.nf[..]) .and_then(|(_, _, txid)| txs.get_mut(txid)) .and_then(|wtx| wtx.notes.iter_mut().find(|nd| &nd.nullifier[..] == &spend.nf[..])) { Some(nd) => nd, None => { warn!("Block {}: tx {} spends a note that is no longer in the wallet; skipping it (a rescan counts it)", block_data.height, tx.txid); continue; } }; // 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(); // An existing entry can come from the mempool monitor (scan_full_mempool_tx), stamped with the // height the server reported before the tx was mined. Record the block that mined it: a reorg of // this block must remove the tx (invalidate_block and the witness clearing key on this field). tx_entry.block = block_data.height; 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(); self.cleanup_incoming_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)>, fee: &u64, 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 RecipientAddress and value to Amount let recepients: Result)>, String> = tos.iter().map(|to| { // Convert string to RecipientAddress 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(addr) => addr, None => { let e = format!("Invalid recipient address: '{}'", to.0); error!("{}", e); return Err(e); } }; // Convert the second tuple element to Amount let value = Amount::from_u64(to.1).expect("Invalid amount value"); Ok((ra, value, to.2.clone())) }).collect(); let recepients = recepients?; // 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 // Select notes to cover the target value println!("{}: Selecting notes", now() - start_time); let target_value = Amount::from_u64(total_value).unwrap() + Amount::from_u64(*fee).unwrap(); let mut builder = Builder::new(height); // A note on t addresses // Funds received by t-addresses can't be explicitly spent in silentdragonxlite. // silentdragonxlite 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, // silentdragonxlite 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::>(); // C246: choose the shielded notes. Only UTXOs the builder will really add (same key predicate as // the add loop below; unconfirmed-spent ones are already out of `tinputs`) count as t value. let t_spendable: Vec<&Utxo> = tinputs.iter().filter(|u| address_to_sk.contains_key(&u.address)).collect(); let t_usable: u64 = t_spendable.iter().map(|u| u.value).sum(); let sapling_out = recepients.iter().filter(|r| match r.0 { address::RecipientAddress::Shielded(_) => true, _ => false, }).count(); let t_out = recepients.len() - sapling_out; let max_spends = cmp::min(MAX_SPENDS_PER_TX, max_spends_for_size(t_spendable.len(), t_out, sapling_out)); let notes: Vec = { // Lock order txs -> zkeys: the same order the old selection and scan_block use. let txs = self.txs.read().unwrap(); let zkeys = self.zkeys.read().unwrap(); // Pass 1: (value, nullifier) of each spendable non-zero note. No witness/extsk clones. let mut cands: Vec = vec![]; let mut owners: Vec<(TxId, usize)> = vec![]; for (txid, tx) in txs.iter() { for (ni, nd) in tx.notes.iter().enumerate() { if nd.note.value == 0 || nd.spent.is_some() || nd.unconfirmed_spent.is_some() { continue; } // Same first-match key lookup the old selection used. let has_extsk = zkeys.iter().find(|zk| zk.extfvk == nd.extfvk) .map(|zk| zk.extsk.is_some()).unwrap_or(false); if SpendableNote::is_spendable(nd, anchor_offset, has_extsk) { cands.push(NoteCand { value: nd.note.value, nullifier: nd.nullifier }); owners.push((*txid, ni)); } } } let picked = match select_spends(&cands, u64::from(target_value), t_usable, max_spends) { Ok(p) => p, Err(SelectError::Insufficient { have }) => { // Wording unchanged: app_network.cpp isTransientVerifiedFundsError matches it. let e = format!( "Insufficient verified funds (have {}, need {:?}). NOTE: funds need {} confirmations before they can be spent.", have.saturating_add(t_usable), target_value, self.config.anchor_offset ); error!("{}", e); return Err(e); } Err(SelectError::TooManyInputs { needed_spends, limit, best_cover }) => { // Must never contain "Insufficient": the GUI would retry it as transient. let e = if limit == 0 { format!("Transaction too large: {} transparent inputs and {} outputs leave no room for a shielded input. Shield the transparent funds first, or send to fewer recipients.", t_spendable.len(), recepients.len()) } else { format!("Too many inputs: this send needs {} notes, but one transaction can spend at most {}. You can send at most {} DRGX right now (after the {} DRGX fee). Send a smaller amount, or send in several steps.", needed_spends, limit, format_drgx(best_cover.saturating_add(t_usable).saturating_sub(*fee)), format_drgx(*fee)) }; error!("{}", e); return Err(e); } }; info!("Selected {} of {} spendable notes (target {:?}, t-usable {})", picked.len(), cands.len(), target_value, t_usable); // Pass 2: build only the chosen notes, with the unchanged constructor. let mut out = Vec::with_capacity(picked.len()); for ci in picked { let (txid, ni) = owners[ci]; let nd = &txs[&txid].notes[ni]; let extsk = zkeys.iter().find(|zk| zk.extfvk == nd.extfvk).and_then(|zk| zk.extsk.clone()); match SpendableNote::from(txid, nd, anchor_offset, &extsk) { Some(s) => out.push(s), None => return Err("Internal error: a selected note is no longer spendable".to_string()), } } out }; // 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::() + t_usable; 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); } let fee_amount = Amount::from_u64(*fee).expect("Invalid fee amount"); builder.set_fee(fee_amount); // Create the transaction println!("{}: Adding {} notes and {} utxos and fee {:?}", now() - start_time, notes.len(), tinputs.len(), fee_amount); 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. The tx is already broadcast, so never panic here: a // reorg or a rescan (clear_blocks) while it was proved can have removed the tx that holds a // selected note or utxo. Skip those with a warning (a panic would poison `txs`; the send went out). let mut txs = self.txs.write().unwrap(); for selected in notes { if let Some(spent_note) = txs.get_mut(&selected.txid) .and_then(|wtx| wtx.notes.iter_mut().find(|nd| &nd.nullifier[..] == &selected.nullifier[..])) { spent_note.unconfirmed_spent = Some(tx.txid()); } else { warn!("Sent {}, but its input note from tx {} is no longer in the wallet (reorg or rescan); not marked pending", tx.txid(), selected.txid); } } // Mark this utxo as unconfirmed spent for utxo in tinputs { if let Some(spent_utxo) = txs.get_mut(&utxo.txid) .and_then(|wtx| wtx.utxos.iter_mut().find(|u| utxo.txid == u.txid && utxo.output_index == u.output_index)) { spent_utxo.unconfirmed_spent = Some(tx.txid()); } else { warn!("Sent {}, but its input utxo {}:{} is no longer in the wallet (reorg or rescan); not marked pending", tx.txid(), utxo.txid, utxo.output_index); } } } // 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() }); } } pub fn cleanup_incoming_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.incoming_mempool_txs.write().unwrap().retain(|_, wtxs| { wtxs.retain(|wtx| current_height < (wtx.block + DEFAULT_TX_EXPIRY_DELTA)); !wtxs.is_empty() // Behalte den Eintrag nur, wenn nicht alle Transaktionen abgelaufen sind }); } { // Remove all txns where the txid is added to the wallet directly self.incoming_mempool_txs.write().unwrap().retain(|txid, _| { self.txs.read().unwrap().get(txid).is_none() }); } } } #[cfg(test)] pub mod tests; #[cfg(test)] mod oracle_tests; #[cfg(test)] mod witness_tests;