The block scan appended each shielded output's leaf to the witness of every live (unspent, not pending) note as it went, one witness at a time on the sync thread, while holding the wallet's txs write lock: live notes x outputs appends per block, all on one core. With 16 or more live notes, the scan now collects the block's leaves in output order and appends them to those witnesses once, at the end of the block, still under the same lock and before anything reads them. When that is at least 128 appends, the witnesses are split into disjoint chunks over min(8, cores, live notes / 8) scoped threads; otherwise the appends run inline. Every witness receives the same leaves in the same order, including a leaf whose epk does not parse; an output whose cmu does not parse still adds nothing. The hashing work is the same, only spread over cores; the speed-up has not been benchmarked yet. No output change. Each qualifying block starts up to min(8, cores) short-lived threads. Tests check that the threaded helper matches the plain double loop byte for byte for 0-64 witnesses, and that the deferred path handles bad-epk and bad-cmu outputs. The 1000-seed oracle passes, as do the suite and 125 oracle seeds pinned to 2 cores, and a differential run against the unmodified backend is byte-identical, apart from the lists cleared two commits earlier, for wallets below and above 16 live notes and blocks of 0, 1 and 50 outputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2803 lines
112 KiB
Rust
2803 lines
112 KiB
Rust
// Copyright The Hush Developers 2019-2022
|
|
// Released under the GPLv3
|
|
use std::time::{SystemTime, Duration};
|
|
use std::io::{self, Read, Write};
|
|
use std::cmp;
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::sync::{Arc, RwLock};
|
|
use std::io::{Error, ErrorKind};
|
|
|
|
use threadpool::ThreadPool;
|
|
use std::sync::mpsc::{channel};
|
|
|
|
use rand::{Rng, rngs::OsRng};
|
|
use subtle::{ConditionallySelectable, ConstantTimeEq, CtOption};
|
|
|
|
use log::{info, warn, error};
|
|
|
|
use protobuf::parse_from_bytes;
|
|
|
|
use libflate::gzip::{Decoder};
|
|
use secp256k1::SecretKey;
|
|
use bip39::{Mnemonic, Language};
|
|
|
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
|
use pairing::bls12_381::{Bls12};
|
|
use sha2::{Sha256, Digest};
|
|
|
|
use sodiumoxide::crypto::secretbox;
|
|
|
|
use zcash_client_backend::{
|
|
encoding::{encode_payment_address, encode_extended_spending_key, encode_extended_full_viewing_key, decode_extended_spending_key, decode_extended_full_viewing_key},
|
|
proto::compact_formats::{CompactBlock, CompactOutput},
|
|
wallet::{WalletShieldedOutput, WalletShieldedSpend}
|
|
};
|
|
|
|
use zcash_primitives::{
|
|
jubjub::fs::Fs,
|
|
block::BlockHash,
|
|
serialize::{Vector},
|
|
transaction::{
|
|
builder::{Builder},
|
|
components::{Amount, OutPoint, TxOut},
|
|
TxId, Transaction,
|
|
},
|
|
sapling::Node,
|
|
merkle_tree::{CommitmentTree, IncrementalWitness},
|
|
legacy::{Script, TransparentAddress},
|
|
note_encryption::{Memo, try_sapling_note_decryption, try_sapling_output_recovery, try_sapling_compact_note_decryption},
|
|
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey, ChildIndex},
|
|
JUBJUB,
|
|
primitives::{PaymentAddress},
|
|
|
|
|
|
};
|
|
|
|
use crate::lightclient::{LightClientConfig};
|
|
|
|
mod data;
|
|
mod extended_key;
|
|
mod utils;
|
|
mod address;
|
|
mod prover;
|
|
mod walletzkey;
|
|
|
|
use data::{BlockData, WalletTx, Utxo, SaplingNoteData, SpendableNote, OutgoingTxMetadata, IncomingTxMetadata};
|
|
use extended_key::{KeyIndex, ExtendedPrivKey};
|
|
use walletzkey::{WalletZKey, WalletTKey, WalletZKeyType};
|
|
|
|
pub const MAX_REORG: usize = 100;
|
|
pub const GAP_RULE_UNUSED_ADDRESSES: usize = 5;
|
|
|
|
fn now() -> f64 {
|
|
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as f64
|
|
}
|
|
|
|
|
|
/// Sha256(Sha256(value))
|
|
pub fn double_sha256(payload: &[u8]) -> Vec<u8> {
|
|
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<u8> = 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<Bls12>),
|
|
Str(String),
|
|
}
|
|
|
|
impl ZAddrFilter {
|
|
fn from_opt(addr: &Option<String>) -> 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<u8>, // Nonce used to encrypt the wallet.
|
|
|
|
seed: [u8; 32], // Seed phrase for this wallet. If wallet is locked, this is 0
|
|
|
|
// List of keys, actually in this wallet. This is a combination of HD keys derived from the seed,
|
|
// viewing keys and imported spending keys.
|
|
zkeys: Arc<RwLock<Vec<WalletZKey>>>,
|
|
|
|
// Transparent keys.
|
|
tkeys: Arc<RwLock<Vec<WalletTKey>>>,
|
|
|
|
blocks: Arc<RwLock<Vec<BlockData>>>,
|
|
pub txs: Arc<RwLock<HashMap<TxId, WalletTx>>>,
|
|
|
|
// Transactions that are only in the mempool, but haven't been confirmed yet.
|
|
// This is not stored to disk.
|
|
pub mempool_txs: Arc<RwLock<HashMap<TxId, WalletTx>>>,
|
|
pub incoming_mempool_txs: Arc<RwLock<HashMap<TxId, Vec<WalletTx>>>>,
|
|
|
|
// 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<RwLock<Vec<Duration>>>,
|
|
}
|
|
|
|
impl LightWallet {
|
|
pub fn serialized_version() -> u64 {
|
|
return 9;
|
|
}
|
|
|
|
fn get_taddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> SecretKey {
|
|
assert_eq!(bip39_seed.len(), 64);
|
|
|
|
let ext_t_key = ExtendedPrivKey::with_seed(bip39_seed).unwrap();
|
|
ext_t_key
|
|
.derive_private_key(KeyIndex::hardened_from_normalize_index(44).unwrap()).unwrap()
|
|
.derive_private_key(KeyIndex::hardened_from_normalize_index(config.get_coin_type()).unwrap()).unwrap()
|
|
.derive_private_key(KeyIndex::hardened_from_normalize_index(0).unwrap()).unwrap()
|
|
.derive_private_key(KeyIndex::Normal(0)).unwrap()
|
|
.derive_private_key(KeyIndex::Normal(pos)).unwrap()
|
|
.private_key
|
|
}
|
|
|
|
|
|
fn get_zaddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) ->
|
|
(ExtendedSpendingKey, ExtendedFullViewingKey, PaymentAddress<Bls12>) {
|
|
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<Bls12> {
|
|
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<String>, config: &LightClientConfig, latest_block: u64, number: u64) -> io::Result<Self> {
|
|
// This is the source entropy that corresponds to the 24-word seed phrase
|
|
let mut seed_bytes = [0u8; 32];
|
|
|
|
if seed_phrase.is_none() {
|
|
// Create a random seed.
|
|
let mut system_rng = OsRng;
|
|
system_rng.fill(&mut seed_bytes);
|
|
} else {
|
|
let phrase = match Mnemonic::from_phrase(seed_phrase.clone().unwrap(), Language::English) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
let e = format!("Error parsing phrase: {}", e);
|
|
error!("{}", e);
|
|
return Err(io::Error::new(ErrorKind::InvalidData, e));
|
|
}
|
|
};
|
|
|
|
seed_bytes.copy_from_slice(&phrase.entropy());
|
|
}
|
|
|
|
// The seed bytes is the raw entropy. To pass it to HD wallet generation,
|
|
// we need to get the 64 byte bip39 entropy
|
|
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&seed_bytes, Language::English).unwrap(), "");
|
|
|
|
// Derive only the first sk and address
|
|
let tpk = LightWallet::get_taddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0);
|
|
let taddr = LightWallet::address_from_prefix_sk(&config.base58_pubkey_address(), &tpk);
|
|
|
|
// TODO: We need to monitor addresses, and always keep 1 "free" address, so
|
|
// users can import a seed phrase and automatically get all used addresses
|
|
let hdkey_num = 0;
|
|
let (extsk, _, _)
|
|
= LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed.as_bytes(), hdkey_num);
|
|
|
|
let lw = LightWallet {
|
|
encrypted: false,
|
|
unlocked: true,
|
|
enc_seed: [0u8; 48],
|
|
nonce: vec![],
|
|
seed: seed_bytes,
|
|
zkeys: Arc::new(RwLock::new(vec![WalletZKey::new_hdkey(hdkey_num, extsk)])),
|
|
tkeys: Arc::new(RwLock::new(vec![WalletTKey::new_hdkey(tpk, taddr)])),
|
|
blocks: Arc::new(RwLock::new(vec![])),
|
|
txs: Arc::new(RwLock::new(HashMap::new())),
|
|
mempool_txs: Arc::new(RwLock::new(HashMap::new())),
|
|
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<R: Read>(mut inp: R, config: &LightClientConfig) -> io::Result<Self> {
|
|
let version = inp.read_u64::<LittleEndian>()?;
|
|
if version > LightWallet::serialized_version() {
|
|
let e = format!("Don't know how to read wallet version {}. Do you have the latest version?", version);
|
|
error!("{}", e);
|
|
return Err(io::Error::new(ErrorKind::InvalidData, e));
|
|
}
|
|
println!("Reading wallet version {}", version);
|
|
info!("Reading wallet version {}", version);
|
|
|
|
// At version 5, we're writing the rest of the file as a compressed stream (gzip)
|
|
let mut reader: Box<dyn Read> = if version !=5 {
|
|
info!("Reading direct");
|
|
Box::new(inp)
|
|
} else {
|
|
info!("Reading libflat");
|
|
Box::new(Decoder::new(inp).unwrap())
|
|
};
|
|
|
|
let encrypted = if version >= 4 {
|
|
reader.read_u8()? > 0
|
|
} else {
|
|
false
|
|
};
|
|
|
|
info!("Wallet Encryption {:?}", encrypted);
|
|
let mut enc_seed = [0u8; 48];
|
|
if version >= 4 {
|
|
reader.read_exact(&mut enc_seed)?;
|
|
}
|
|
|
|
let nonce = if version >= 4 {
|
|
Vector::read(&mut reader, |r| r.read_u8())?
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
// Seed
|
|
let mut seed_bytes = [0u8; 32];
|
|
reader.read_exact(&mut seed_bytes)?;
|
|
|
|
let zkeys = if version <= 6 {
|
|
// Up until version 6, the wallet keys were written out individually
|
|
// Read the spending keys
|
|
let extsks = Vector::read(&mut reader, |r| ExtendedSpendingKey::read(r))?;
|
|
|
|
let extfvks = if version >= 4 {
|
|
// Read the viewing keys
|
|
Vector::read(&mut reader, |r| ExtendedFullViewingKey::read(r))?
|
|
} else {
|
|
// Calculate the viewing keys
|
|
extsks.iter().map(|sk| ExtendedFullViewingKey::from(sk))
|
|
.collect::<Vec<ExtendedFullViewingKey>>()
|
|
};
|
|
|
|
// Calculate the addresses
|
|
let addresses = extfvks.iter().map( |fvk| fvk.default_address().unwrap().1 )
|
|
.collect::<Vec<PaymentAddress<Bls12>>>();
|
|
|
|
// 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::<Vec<io::Result<WalletZKey>>>()
|
|
} 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::<Vec<io::Result<WalletZKey>>>()
|
|
};
|
|
|
|
// 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::<io::Result<_>>()?
|
|
} else {
|
|
// After version 6, we read the WalletZKey structs directly
|
|
Vector::read(&mut reader, |r| WalletZKey::read(r))?
|
|
};
|
|
|
|
// Calculate the addresses
|
|
|
|
let wallet_tkeys = if version >= 9 {
|
|
Vector::read(&mut reader, |r| {
|
|
WalletTKey::read(r)
|
|
})?
|
|
} else {
|
|
|
|
let tkeys = Vector::read(&mut reader, |r| {
|
|
let mut tpk_bytes = [0u8; 32];
|
|
r.read_exact(&mut tpk_bytes)?;
|
|
secp256k1::SecretKey::from_slice(&tpk_bytes).map_err(|e| io::Error::new(ErrorKind::InvalidData, e))
|
|
})?;
|
|
|
|
let taddresses = if version >= 4 {
|
|
// Read the addresses
|
|
Vector::read(&mut reader, |r| utils::read_string(r))?
|
|
} else {
|
|
// Calculate the addresses
|
|
tkeys.iter().map(|sk| LightWallet::address_from_prefix_sk(&config.base58_pubkey_address(), sk)).collect()
|
|
};
|
|
|
|
tkeys.iter().zip(taddresses.iter()).map(|(k, a)|
|
|
WalletTKey::new_hdkey(*k, a.clone())
|
|
).collect()
|
|
};
|
|
|
|
let blocks = Vector::read(&mut reader, |r| BlockData::read(r))?;
|
|
|
|
let txs_tuples = Vector::read(&mut reader, |r| {
|
|
let mut txid_bytes = [0u8; 32];
|
|
r.read_exact(&mut txid_bytes)?;
|
|
|
|
Ok((TxId{0: txid_bytes}, WalletTx::read(r).unwrap()))
|
|
})?;
|
|
let txs = txs_tuples.into_iter().collect::<HashMap<TxId, WalletTx>>();
|
|
|
|
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::<LittleEndian>()?;
|
|
|
|
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,
|
|
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<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
|
if self.encrypted && self.unlocked {
|
|
return Err(Error::new(ErrorKind::InvalidInput,
|
|
format!("Cannot write while wallet is unlocked while encrypted.")));
|
|
}
|
|
|
|
// Write the version
|
|
writer.write_u64::<LittleEndian>(LightWallet::serialized_version())?;
|
|
|
|
// Write if it is locked
|
|
writer.write_u8(if self.encrypted {1} else {0})?;
|
|
|
|
// Write the encrypted seed bytes
|
|
writer.write_all(&self.enc_seed)?;
|
|
|
|
// Write the nonce
|
|
Vector::write(&mut writer, &self.nonce, |w, b| w.write_u8(*b))?;
|
|
|
|
// Write the seed
|
|
writer.write_all(&self.seed)?;
|
|
|
|
// Flush after writing the seed, so in case of a disaster, we can still recover the seed.
|
|
writer.flush()?;
|
|
|
|
// Write all the wallet's keys
|
|
Vector::write(&mut writer, &self.zkeys.read().unwrap(),
|
|
|w, zk| zk.write(w)
|
|
)?;
|
|
|
|
// Write the transparent private keys
|
|
Vector::write(&mut writer, &self.tkeys.read().unwrap(),
|
|
|w, tk| tk.write(w)
|
|
)?;
|
|
|
|
Vector::write(&mut writer, &self.blocks.read().unwrap(), |w, b| b.write(w))?;
|
|
|
|
// The hashmap, write as a set of tuples. Store them sorted so that wallets are
|
|
// deterministically saved
|
|
{
|
|
let txlist = self.txs.read().unwrap();
|
|
let mut txns = txlist.iter().collect::<Vec<(&TxId, &WalletTx)>>();
|
|
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::<LittleEndian>(self.get_birthday())
|
|
}
|
|
|
|
pub fn note_address(hrp: &str, note: &SaplingNoteData) -> Option<String> {
|
|
// 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::<Vec<u64>>();
|
|
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::<Vec<(String, String, String)>>();
|
|
|
|
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::<Vec<(String, String)>>()
|
|
}
|
|
|
|
/// Adds a new z address to the wallet. This will derive a new address from the seed
|
|
/// at the next position and add it to the wallet.
|
|
/// NOTE: This does NOT rescan
|
|
pub fn add_zaddr(&self) -> String {
|
|
if !self.unlocked {
|
|
return "Error: Can't add key while wallet is locked".to_string();
|
|
}
|
|
|
|
if self.encrypted {
|
|
return "Error: Can't add key while wallet is encrypted".to_string();
|
|
}
|
|
|
|
// Find the highest pos we have
|
|
let pos = self.zkeys.read().unwrap().iter()
|
|
.filter(|zk| zk.hdkey_num.is_some())
|
|
.max_by(|zk1, zk2| zk1.hdkey_num.unwrap().cmp(&zk2.hdkey_num.unwrap()))
|
|
.map_or(0, |zk| zk.hdkey_num.unwrap() + 1);
|
|
|
|
|
|
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), "");
|
|
|
|
let (extsk, _, _) =
|
|
LightWallet::get_zaddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
|
|
|
|
// let zaddr = encode_payment_address(self.config.hrp_sapling_address(), &address);
|
|
let newkey = WalletZKey::new_hdkey(pos, extsk);
|
|
self.zkeys.write().unwrap().push(newkey.clone());
|
|
|
|
encode_payment_address(self.config.hrp_sapling_address(), &newkey.zaddress)
|
|
}
|
|
|
|
// Add a new Sietch Addr. This will derive a new zdust address from manipluated seed
|
|
pub fn add_zaddrdust(&self) -> String {
|
|
|
|
let mut seed_bytes = [0u8; 32];
|
|
|
|
|
|
// Use random generator to create a new Sietch seed
|
|
|
|
let mut rng = rand::thread_rng();
|
|
let letter: String = rng.gen_range(b'A', b'Z').to_string();
|
|
let number: String = rng.gen_range(0, 999999).to_string();
|
|
let s = format!("{}{:06}", letter, number);
|
|
let my_string = String::from(s);
|
|
let dust: &str = &my_string;
|
|
|
|
|
|
let mut system_rng = OsRng;
|
|
system_rng.fill(&mut seed_bytes);
|
|
|
|
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&seed_bytes, Language::English).unwrap(), dust);
|
|
|
|
let zdustaddress = LightWallet::get_sietch_from_bip39seed(&bip39_seed.as_bytes());
|
|
|
|
let zdust = encode_payment_address("zs", &zdustaddress);
|
|
|
|
|
|
zdust
|
|
}
|
|
|
|
/// Add a new t address to the wallet. This will derive a new address from the seed
|
|
/// at the next position.
|
|
/// NOTE: This will not rescan the wallet
|
|
pub fn add_taddr(&self) -> String {
|
|
if !self.unlocked {
|
|
return "Error: Can't add key while wallet is locked".to_string();
|
|
}
|
|
|
|
if self.encrypted {
|
|
return "Error: Can't add key while wallet is encrypted".to_string();
|
|
}
|
|
|
|
let pos = self.tkeys.read().unwrap().len() as u32;
|
|
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), "");
|
|
|
|
let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
|
|
let address = self.address_from_sk(&sk);
|
|
|
|
self.tkeys.write().unwrap().push(WalletTKey::new_hdkey(sk, address.clone()));
|
|
|
|
address
|
|
}
|
|
|
|
pub fn import_taddr(&mut self, sk: String, birthday: u64) -> String {
|
|
if !self.unlocked {
|
|
return "Error: Can't add key while wallet is locked".to_string();
|
|
}
|
|
|
|
//// Decode Wif to base58 to hex
|
|
let sk_to_bs58 = bs58::decode(sk).into_vec().unwrap();
|
|
|
|
let bs58_to_hex = hex::encode(sk_to_bs58);
|
|
|
|
//// Manipulate string, to exclude last 4 bytes (checksum bytes), first 2 bytes (secretkey prefix) and the compressed flag (works only for compressed Wifs!)
|
|
|
|
let slice_sk = &bs58_to_hex[2..66];
|
|
|
|
//// Get the SecretKey from slice
|
|
let secret_key = SecretKey::from_slice(&hex::decode(slice_sk).unwrap());
|
|
|
|
let sk_raw = secret_key.unwrap();
|
|
|
|
//// Make sure the key doesn't already exist
|
|
if self.tkeys.read().unwrap().iter().find(|&wk| wk.tkey.is_some() && wk.tkey.as_ref().unwrap() == &sk_raw.clone()).is_some() {
|
|
return "Error: Key already exists".to_string();
|
|
}
|
|
//// Get the taddr from key
|
|
let address = self.address_from_sk(&sk_raw);
|
|
|
|
//// Add to tkeys
|
|
self.tkeys.write().unwrap().push(WalletTKey::import_hdkey(sk_raw , address.clone()));
|
|
|
|
// Adjust wallet birthday
|
|
if birthday < self.birthday {
|
|
self.birthday = if birthday < self.config.sapling_activation_height {self.config.sapling_activation_height} else {birthday};
|
|
}
|
|
|
|
address
|
|
}
|
|
|
|
// Add a new imported spending key to the wallet
|
|
/// NOTE: This will not rescan the wallet
|
|
pub fn add_imported_sk(&mut self, sk: String, birthday: u64) -> String {
|
|
if !self.unlocked {
|
|
return "Error: Can't add key while wallet is locked".to_string();
|
|
}
|
|
|
|
// First, try to interpret the key
|
|
let extsk = match decode_extended_spending_key(self.config.hrp_sapling_private_key(), &sk) {
|
|
Ok(Some(k)) => k,
|
|
Ok(None) => return format!("Error: Couldn't decode spending key"),
|
|
Err(e) => return format!("Error importing spending key: {}", e)
|
|
};
|
|
|
|
// Make sure the key doesn't already exist
|
|
if self.zkeys.read().unwrap().iter().find(|&wk| wk.extsk.is_some() && wk.extsk.as_ref().unwrap() == &extsk.clone()).is_some() {
|
|
return "Error: Key already exists".to_string();
|
|
}
|
|
|
|
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
|
let zaddress = {
|
|
let mut zkeys = self.zkeys.write().unwrap();
|
|
let maybe_existing_zkey = zkeys.iter_mut().find(|wk| wk.extfvk == extfvk);
|
|
|
|
// If the viewing key exists, and is now being upgraded to the spending key, replace it in-place
|
|
if maybe_existing_zkey.is_some() {
|
|
let mut existing_zkey = maybe_existing_zkey.unwrap();
|
|
existing_zkey.extsk = Some(extsk);
|
|
existing_zkey.keytype = WalletZKeyType::ImportedSpendingKey;
|
|
existing_zkey.zaddress.clone()
|
|
} else {
|
|
let newkey = WalletZKey::new_imported_sk(extsk);
|
|
zkeys.push(newkey.clone());
|
|
newkey.zaddress
|
|
}
|
|
};
|
|
|
|
// Adjust wallet birthday
|
|
if birthday < self.birthday {
|
|
self.birthday = if birthday < self.config.sapling_activation_height {self.config.sapling_activation_height} else {birthday};
|
|
}
|
|
|
|
encode_payment_address(self.config.hrp_sapling_address(), &zaddress)
|
|
}
|
|
|
|
// Add a new imported viewing key to the wallet
|
|
/// NOTE: This will not rescan the wallet
|
|
pub fn add_imported_vk(&mut self, vk: String, birthday: u64) -> String {
|
|
if !self.unlocked {
|
|
return "Error: Can't add key while wallet is locked".to_string();
|
|
}
|
|
|
|
// First, try to interpret the key
|
|
let extfvk = match decode_extended_full_viewing_key(self.config.hrp_sapling_viewing_key(), &vk) {
|
|
Ok(Some(k)) => k,
|
|
Ok(None) => return format!("Error: Couldn't decode viewing key"),
|
|
Err(e) => return format!("Error importing viewing key: {}", e)
|
|
};
|
|
|
|
// Make sure the key doesn't already exist
|
|
if self.zkeys.read().unwrap().iter().find(|wk| wk.extfvk == extfvk.clone()).is_some() {
|
|
return "Error: Key already exists".to_string();
|
|
}
|
|
|
|
let newkey = WalletZKey::new_imported_viewkey(extfvk);
|
|
self.zkeys.write().unwrap().push(newkey.clone());
|
|
|
|
// Adjust wallet birthday
|
|
if birthday < self.birthday {
|
|
self.birthday = if birthday < self.config.sapling_activation_height {self.config.sapling_activation_height} else {birthday};
|
|
}
|
|
|
|
encode_payment_address(self.config.hrp_sapling_address(), &newkey.zaddress)
|
|
}
|
|
|
|
/// Clears all the downloaded blocks and resets the state back to the initial block.
|
|
/// After this, the wallet's initial state will need to be set
|
|
/// and the wallet will need to be rescanned
|
|
pub fn clear_blocks(&self) {
|
|
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<String> {
|
|
self.tkeys.read().unwrap()
|
|
.iter()
|
|
.map(|wtx| wtx.address.clone()).collect()
|
|
}
|
|
|
|
pub fn get_all_zaddresses(&self) -> Vec<String> {
|
|
self.zkeys.read().unwrap().iter().map( |zk| {
|
|
encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress)
|
|
}).collect()
|
|
}
|
|
|
|
pub fn memo_str(memo: &Option<Memo>) -> Option<String> {
|
|
match memo {
|
|
Some(memo) => {
|
|
match memo.to_utf8() {
|
|
Some(Ok(memo_str)) => Some(memo_str),
|
|
_ => None
|
|
}
|
|
}
|
|
_ => None
|
|
}
|
|
}
|
|
|
|
pub fn address_from_prefix_sk(prefix: &[u8; 1], sk: &secp256k1::SecretKey) -> String {
|
|
let secp = secp256k1::Secp256k1::new();
|
|
let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk);
|
|
|
|
// Encode into t address
|
|
let mut hash160 = ripemd160::Ripemd160::new();
|
|
hash160.input(Sha256::digest(&pk.serialize()[..].to_vec()));
|
|
|
|
hash160.result().to_base58check(prefix, &[])
|
|
}
|
|
|
|
pub fn address_from_sk(&self, sk: &secp256k1::SecretKey) -> String {
|
|
LightWallet::address_from_prefix_sk(&self.config.base58_pubkey_address(), sk)
|
|
}
|
|
|
|
pub fn address_from_pubkeyhash(&self, ta: Option<TransparentAddress>) -> Option<String> {
|
|
match ta {
|
|
Some(TransparentAddress::PublicKey(hash)) => {
|
|
Some(hash.to_base58check(&self.config.base58_pubkey_address(), &[]))
|
|
},
|
|
Some(TransparentAddress::Script(hash)) => {
|
|
Some(hash.to_base58check(&self.config.base58_script_address(), &[]))
|
|
},
|
|
_ => None
|
|
}
|
|
}
|
|
|
|
pub fn get_seed_phrase(&self) -> String {
|
|
if !self.unlocked {
|
|
return "".to_string();
|
|
}
|
|
|
|
Mnemonic::from_entropy(&self.seed,
|
|
Language::English,
|
|
).unwrap().phrase().to_string()
|
|
}
|
|
|
|
pub fn encrypt(&mut self, passwd: String) -> io::Result<()> {
|
|
|
|
|
|
if self.encrypted {
|
|
return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is already encrypted"));
|
|
}
|
|
|
|
// Get the doublesha256 of the password, which is the right length
|
|
let key = secretbox::Key::from_slice(&double_sha256(passwd.as_bytes())).unwrap();
|
|
let nonce = secretbox::gen_nonce();
|
|
|
|
let cipher = secretbox::seal(&self.seed, &nonce, &key);
|
|
|
|
self.enc_seed.copy_from_slice(&cipher);
|
|
self.nonce = nonce.as_ref().to_vec();
|
|
|
|
// Encrypt the individual keys
|
|
|
|
self.tkeys.write().unwrap().iter_mut()
|
|
.map(|k| k.encrypt(&key))
|
|
.collect::<io::Result<Vec<()>>>()?;
|
|
|
|
self.zkeys.write().unwrap().iter_mut()
|
|
.map(|k| k.encrypt(&key))
|
|
.collect::<io::Result<Vec<()>>>()?;
|
|
|
|
self.encrypted = true;
|
|
self.lock()?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn lock(&mut self) -> io::Result<()> {
|
|
if !self.encrypted {
|
|
return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is not encrypted"));
|
|
}
|
|
|
|
if !self.unlocked {
|
|
return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is already locked"));
|
|
}
|
|
|
|
// Empty the seed and the secret keys
|
|
self.seed.copy_from_slice(&[0u8; 32]);
|
|
|
|
// Remove all the private key from the tkeys
|
|
self.tkeys.write().unwrap().iter_mut().map(|tk| {
|
|
tk.lock()
|
|
}).collect::<io::Result<Vec<_>>>()?;
|
|
|
|
// Remove all the private key from the zkeys
|
|
self.zkeys.write().unwrap().iter_mut().map(|zk| {
|
|
zk.lock()
|
|
}).collect::<io::Result<Vec<_>>>()?;
|
|
|
|
self.unlocked = false;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn unlock(&mut self, passwd: String) -> io::Result<()> {
|
|
|
|
|
|
if !self.encrypted {
|
|
return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is not encrypted"));
|
|
}
|
|
|
|
if self.encrypted && self.unlocked {
|
|
return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is already unlocked"));
|
|
}
|
|
|
|
// Get the doublesha256 of the password, which is the right length
|
|
let key = secretbox::Key::from_slice(&double_sha256(passwd.as_bytes())).unwrap();
|
|
let nonce = secretbox::Nonce::from_slice(&self.nonce).unwrap();
|
|
|
|
let seed = match secretbox::open(&self.enc_seed, &nonce, &key) {
|
|
Ok(s) => s,
|
|
Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));}
|
|
};
|
|
|
|
// Now that we have the seed, we'll generate the extsks and tkeys, and verify the fvks and addresses
|
|
// respectively match
|
|
|
|
// The seed bytes is the raw entropy. To pass it to HD wallet generation,
|
|
// we need to get the 64 byte bip39 entropy
|
|
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&seed, Language::English).unwrap(), "");
|
|
|
|
// Go over the tkeys, and add the keys again
|
|
self.tkeys.write().unwrap().iter_mut().map(|tk| {
|
|
tk.unlock(&key)
|
|
}).collect::<io::Result<Vec<()>>>()?;
|
|
|
|
// Go over the zkeys, and add the spending keys again
|
|
self.zkeys.write().unwrap().iter_mut().map(|zk| {
|
|
zk.unlock(&self.config, bip39_seed.as_bytes(), &key)
|
|
}).collect::<io::Result<Vec<()>>>()?;
|
|
|
|
// Everything checks out, so we'll update our wallet with the decrypted values
|
|
self.seed.copy_from_slice(&seed);
|
|
|
|
self.encrypted = true;
|
|
self.unlocked = true;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Removing encryption means unlocking it and setting the self.encrypted = false,
|
|
// permanantly removing the encryption
|
|
pub fn remove_encryption(&mut self, passwd: String) -> io::Result<()> {
|
|
if !self.encrypted {
|
|
return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is not encrypted"));
|
|
}
|
|
|
|
// Unlock the wallet if it's locked
|
|
if !self.unlocked {
|
|
self.unlock(passwd)?;
|
|
}
|
|
|
|
// Remove encryption from individual tkeys
|
|
self.tkeys.write().unwrap().iter_mut().map(|tk| {
|
|
tk.remove_encryption()
|
|
}).collect::<io::Result<Vec<()>>>()?;
|
|
|
|
// Remove encryption from individual zkeys
|
|
self.zkeys.write().unwrap().iter_mut().map(|zk| {
|
|
zk.remove_encryption()
|
|
}).collect::<io::Result<Vec<()>>>()?;
|
|
|
|
// Permanantly remove the encryption
|
|
self.encrypted = false;
|
|
self.nonce = vec![];
|
|
self.enc_seed.copy_from_slice(&[0u8; 48]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn is_encrypted(&self) -> bool {
|
|
return self.encrypted;
|
|
}
|
|
|
|
pub fn is_unlocked_for_spending(&self) -> bool {
|
|
return self.unlocked;
|
|
}
|
|
|
|
pub fn zbalance(&self, addr: Option<String>) -> 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::<u64>()
|
|
})
|
|
.sum::<u64>()
|
|
}
|
|
|
|
// Get all (unspent) utxos. Unconfirmed spent utxos are included
|
|
pub fn get_utxos(&self) -> Vec<Utxo> {
|
|
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::<Vec<Utxo>>()
|
|
}
|
|
|
|
pub fn tbalance(&self, addr: Option<String>) -> u64 {
|
|
self.get_utxos().iter()
|
|
.filter(|utxo| {
|
|
match addr.clone() {
|
|
Some(a) => utxo.address == a,
|
|
None => true,
|
|
}
|
|
})
|
|
.map(|utxo| utxo.value )
|
|
.sum::<u64>() as u64
|
|
}
|
|
|
|
pub fn verified_zbalance(&self, addr: Option<String>) -> 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::<u64>()
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
.sum::<u64>()
|
|
}
|
|
|
|
pub fn spendable_zbalance(&self, addr: Option<String>) -> 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::<u64>()
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
.sum::<u64>() as u64
|
|
}
|
|
|
|
pub fn unconfirmed_zbalance(&self, addr: Option<String>) -> 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::<u64>()
|
|
})
|
|
.sum::<u64>()
|
|
}
|
|
|
|
pub fn have_spendingkey_for_extfvk(&self, extfvk: &ExtendedFullViewingKey) -> bool {
|
|
match self.zkeys.read().unwrap().iter().find(|zk| zk.extfvk == *extfvk) {
|
|
None => false,
|
|
Some(zk) => zk.have_spending_key()
|
|
}
|
|
}
|
|
|
|
pub fn have_spending_key_for_zaddress(&self, address: &String) -> bool {
|
|
match self.zkeys.read().unwrap().iter()
|
|
.find(|zk| encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress) == *address)
|
|
{
|
|
None => false,
|
|
Some(zk) => zk.have_spending_key()
|
|
}
|
|
}
|
|
|
|
/// 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<Bls12>)> = 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<String> {
|
|
let hrp = self.config.hrp_sapling_address();
|
|
let mut first: HashMap<String, bool> = 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::<Vec<String>>()
|
|
};
|
|
|
|
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::<Vec<String>>()
|
|
};
|
|
|
|
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::<Vec<_>>();
|
|
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::<HashSet<String>>();
|
|
for vout in tx.vout.iter() {
|
|
let taddr = self.address_from_pubkeyhash(vout.script_pubkey.address());
|
|
if taddr.is_some() && !wallet_taddrs.contains(&taddr.clone().unwrap()) {
|
|
let taddr = taddr.unwrap();
|
|
// Add it to outgoing metadata
|
|
let mut txs = self.txs.write().unwrap();
|
|
if txs.get(&tx.txid()).unwrap().outgoing_metadata.iter()
|
|
.find(|om|
|
|
om.address == taddr && Amount::from_u64(om.value).unwrap() == vout.value)
|
|
.is_some() {
|
|
warn!("Duplicate outgoing metadata");
|
|
continue;
|
|
}
|
|
// Write the outgoing metadata
|
|
txs.get_mut(&tx.txid()).unwrap()
|
|
.outgoing_metadata
|
|
.push(OutgoingTxMetadata{
|
|
address: taddr,
|
|
value: vout.value.into(),
|
|
memo: Memo::default(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Scan shielded sapling outputs to see if anyone of them is us, and if it is, extract the memo
|
|
for output in tx.shielded_outputs.iter() {
|
|
let ivks: Vec<_> = self.zkeys.read().unwrap().iter()
|
|
.map(|zk| zk.extfvk.fvk.vk.ivk()
|
|
).collect();
|
|
let cmu = output.cmu;
|
|
let ct = output.enc_ciphertext;
|
|
// Search all of our keys
|
|
for ivk in ivks {
|
|
let epk_prime = output.ephemeral_key.as_prime_order(&JUBJUB).unwrap();
|
|
let (note, _to, memo) = match try_sapling_note_decryption(&ivk, &epk_prime, &cmu, &ct) {
|
|
Some(ret) => ret,
|
|
None => continue,
|
|
};
|
|
if memo.to_utf8().is_some() {
|
|
// info!("A sapling note was sent to wallet in {} that had a memo", tx.txid());
|
|
// Do it in a short scope because of the write lock.
|
|
let mut txs = self.txs.write().unwrap();
|
|
// Update memo if we have this Tx.
|
|
match txs.get_mut(&tx.txid())
|
|
.and_then(|t| {
|
|
t.notes.iter_mut().find(|nd| nd.note == note)
|
|
}) {
|
|
None => {
|
|
info!("No txid matched for incoming sapling funds while updating memo");
|
|
()
|
|
},
|
|
Some(nd) => {
|
|
nd.memo = Some(memo)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Also scan the output to see if it can be decoded with our OutgoingViewKey
|
|
// If it can, then we sent this transaction, so we should be able to get
|
|
// the memo and value for our records
|
|
|
|
// 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,
|
|
&output.ephemeral_key.as_prime_order(&JUBJUB).unwrap(),
|
|
&output.enc_ciphertext,
|
|
&output.out_ciphertext) {
|
|
Some((note, payment_address, memo)) => {
|
|
let address = encode_payment_address(self.config.hrp_sapling_address(),
|
|
&payment_address);
|
|
|
|
// Check if this 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 epk_prime = output.ephemeral_key.as_prime_order(&JUBJUB).unwrap();
|
|
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() {
|
|
let ivks: Vec<_> = self.zkeys.read().unwrap().iter()
|
|
.map(|zk| zk.extfvk.fvk.vk.ivk())
|
|
.collect();
|
|
|
|
let cmu = output.cmu;
|
|
let ct = output.enc_ciphertext;
|
|
|
|
// Search all of our keys
|
|
for ivk in ivks {
|
|
let epk_prime = output.ephemeral_key.as_prime_order(&JUBJUB).unwrap();
|
|
let (note, _to, memo) = match try_sapling_note_decryption(&ivk, &epk_prime, &cmu, &ct) {
|
|
Some(ret) => ret,
|
|
None => continue,
|
|
};
|
|
|
|
if 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<i32> {
|
|
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<TxId, WalletTx>, 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::<HashSet<TxId>>();
|
|
|
|
for txid in &txids_to_remove {
|
|
txs.remove(&txid);
|
|
}
|
|
|
|
// We also need to update any sapling note data and utxos in existing transactions that
|
|
// were spent in any of the txids that were removed
|
|
txs.values_mut()
|
|
.for_each(|wtx| {
|
|
wtx.notes.iter_mut()
|
|
.for_each(|nd| {
|
|
if nd.spent.is_some() && txids_to_remove.contains(&nd.spent.unwrap()) {
|
|
nd.spent = None;
|
|
}
|
|
|
|
if nd.unconfirmed_spent.is_some() && txids_to_remove.contains(&nd.spent.unwrap()) {
|
|
nd.unconfirmed_spent = None;
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
// Of the notes that still remain, unroll the witness.
|
|
// Remove `num_invalidated` items from the witness
|
|
{
|
|
let mut txs = 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<Node>],
|
|
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");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 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<Node>,
|
|
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
|
|
defer_existing: bool,
|
|
deferred_nodes: &mut Vec<Node>,
|
|
block_witnesses: &mut [&mut IncrementalWitness<Node>],
|
|
new_witnesses: &mut [&mut IncrementalWitness<Node>],
|
|
pool: &ThreadPool
|
|
) -> Option<WalletShieldedOutput> {
|
|
// 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);
|
|
let node = Node::new(cmu.into());
|
|
if defer_existing {
|
|
deferred_nodes.push(node);
|
|
} else {
|
|
for witness in existing_witnesses {
|
|
witness.append(node).unwrap();
|
|
}
|
|
}
|
|
for witness in block_witnesses {
|
|
witness.append(node).unwrap();
|
|
}
|
|
for witness in new_witnesses {
|
|
witness.append(node).unwrap();
|
|
}
|
|
tree.append(node).unwrap();
|
|
return None;
|
|
}
|
|
};
|
|
let ct = output.ciphertext;
|
|
|
|
let (tx, rx) = channel();
|
|
ivks.iter().enumerate().for_each(|(account, ivk)| {
|
|
// Clone all values for passing to the closure
|
|
let ivk = ivk.clone();
|
|
let epk = epk.clone();
|
|
let ct = ct.clone();
|
|
let tx = tx.clone();
|
|
|
|
pool.execute(move || {
|
|
let m = try_sapling_compact_note_decryption(&ivk, &epk, &cmu, &ct);
|
|
let r = match m {
|
|
Some((note, to)) => {
|
|
tx.send(Some(Some((note, to, account))))
|
|
},
|
|
None => {
|
|
tx.send(Some(None))
|
|
}
|
|
};
|
|
|
|
match r {
|
|
Ok(_) => {},
|
|
Err(e) => println!("Send error {:?}", e)
|
|
}
|
|
});
|
|
});
|
|
|
|
// Increment tree and witnesses
|
|
let node = Node::new(cmu.into());
|
|
if defer_existing {
|
|
deferred_nodes.push(node);
|
|
} else {
|
|
for witness in existing_witnesses {
|
|
witness.append(node).unwrap();
|
|
}
|
|
}
|
|
for witness in block_witnesses {
|
|
witness.append(node).unwrap();
|
|
}
|
|
for witness in new_witnesses {
|
|
witness.append(node).unwrap();
|
|
}
|
|
tree.append(node).unwrap();
|
|
|
|
// Collect all the RXs and fine if there was a valid result somewhere
|
|
let mut wsos = vec![];
|
|
for _i in 0..ivks.len() {
|
|
let n = rx.recv().unwrap();
|
|
let epk = epk.clone();
|
|
|
|
let wso = match n {
|
|
None => panic!("Got a none!"),
|
|
Some(None) => None,
|
|
Some(Some((note, to, account))) => {
|
|
// A note is marked as "change" if the account that received it
|
|
// also spent notes in the same transaction. This will catch,
|
|
// for instance:
|
|
// - Change created by spending fractions of notes.
|
|
// - Notes created by consolidation transactions.
|
|
// - Notes sent from one account to itself.
|
|
//let is_change = spent_from_accounts.contains(&account);
|
|
|
|
Some(WalletShieldedOutput {
|
|
index, cmu, epk, account, note, to, is_change: false,
|
|
witness: IncrementalWitness::from_tree(tree),
|
|
})
|
|
}
|
|
};
|
|
wsos.push(wso);
|
|
}
|
|
|
|
match wsos.into_iter().find(|wso| wso.is_some()) {
|
|
Some(Some(wso)) => Some(wso),
|
|
_ => None
|
|
}
|
|
}
|
|
|
|
/// Scans a [`CompactBlock`] with a set of [`ExtendedFullViewingKey`]s.
|
|
///
|
|
/// Returns a vector of [`WalletTx`]s belonging to any of the given
|
|
/// [`ExtendedFullViewingKey`]s, and the corresponding new [`IncrementalWitness`]es.
|
|
///
|
|
/// The given [`CommitmentTree`] and existing [`IncrementalWitness`]es are
|
|
/// incremented appropriately.
|
|
pub fn scan_block_internal(
|
|
&self,
|
|
block: CompactBlock,
|
|
extfvks: &[ExtendedFullViewingKey],
|
|
nullifiers: Vec<(Vec<u8>, usize)>,
|
|
tree: &mut CommitmentTree<Node>,
|
|
existing_witnesses: &mut [&mut IncrementalWitness<Node>],
|
|
pool: &ThreadPool
|
|
) -> Vec<zcash_client_backend::wallet::WalletTx> {
|
|
let mut wtxs: Vec<zcash_client_backend::wallet::WalletTx> = vec![];
|
|
let ivks = extfvks.iter().map(|extfvk| extfvk.fvk.vk.ivk()).collect::<Vec<_>>();
|
|
|
|
// 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<Node> = 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<WalletShieldedOutput> = 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<Vec<TxId>, 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<Vec<TxId>, i32> {
|
|
let block: CompactBlock = match parse_from_bytes(block_bytes) {
|
|
Ok(block) => block,
|
|
Err(e) => {
|
|
error!("Could not parse CompactBlock from bytes: {}", e);
|
|
return Err(-1);
|
|
}
|
|
};
|
|
|
|
// Scanned blocks MUST be height-sequential.
|
|
let height = block.get_height() as i32;
|
|
if height == self.last_scanned_height() {
|
|
// If the last scanned block is rescanned, check it still matches.
|
|
if let Some(hash) = self.blocks.read().unwrap().last().map(|block| block.hash) {
|
|
if block.hash() != hash {
|
|
warn!("Likely reorg. Block hash does not match for block {}. {} vs {}", height, block.hash(), hash);
|
|
return Err(height);
|
|
}
|
|
}
|
|
return Ok(vec![]);
|
|
} else if height != (self.last_scanned_height() + 1) {
|
|
error!(
|
|
"Block is not height-sequential (expected {}, found {})",
|
|
self.last_scanned_height() + 1,
|
|
height
|
|
);
|
|
return Err(self.last_scanned_height());
|
|
}
|
|
|
|
// Check to see that the previous block hash matches
|
|
if let Some(hash) = self.blocks.read().unwrap().last().map(|block| block.hash) {
|
|
if block.prev_hash() != hash {
|
|
warn!("Likely reorg. Prev block hash does not match for block {}. {} vs {}", height, block.prev_hash(), hash);
|
|
return Err(height-1);
|
|
}
|
|
}
|
|
|
|
// Get the most recent scanned data.
|
|
let mut block_data = BlockData {
|
|
height,
|
|
hash: block.hash(),
|
|
tree: self
|
|
.blocks
|
|
.read()
|
|
.unwrap()
|
|
.last()
|
|
.map(|block| block.tree.clone())
|
|
.unwrap_or(CommitmentTree::new()),
|
|
};
|
|
|
|
// These are filled in inside the block
|
|
let new_txs;
|
|
let nfs: Vec<_>;
|
|
|
|
// 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 (nothing to do for notes with no history)
|
|
if let Some(witness) = nd.witnesses.last() {
|
|
let clone = witness.clone();
|
|
nd.witnesses.push(clone);
|
|
// 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::<Vec<_>>();
|
|
let extfvks: Vec<ExtendedFullViewingKey> = self.zkeys.read().unwrap().iter().map(|zk| zk.extfvk.clone()).collect();
|
|
|
|
// Create a single mutable slice of all the newly-added witnesses.
|
|
let mut witness_refs: Vec<_> = txs
|
|
.values_mut()
|
|
.map(|tx| tx.notes.iter_mut().filter_map(
|
|
|nd| if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { nd.witnesses.last_mut() } else { None }))
|
|
.flatten()
|
|
.collect();
|
|
|
|
self.scan_block_internal(
|
|
block.clone(),
|
|
&extfvks,
|
|
nf_refs,
|
|
&mut block_data.tree,
|
|
&mut witness_refs[..],
|
|
pool,
|
|
)
|
|
};
|
|
}
|
|
|
|
|
|
// If this block had any new Txs, return the list of ALL txids in this block,
|
|
// so the wallet can fetch them all as a decoy.
|
|
let all_txs = if !new_txs.is_empty() {
|
|
block.vtx.iter().map(|vtx| {
|
|
let mut t = [0u8; 32];
|
|
t.copy_from_slice(&vtx.hash[..]);
|
|
TxId{0: t}
|
|
}).collect::<Vec<TxId>>()
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
for tx in new_txs {
|
|
// Create a write lock
|
|
let mut txs = self.txs.write().unwrap();
|
|
|
|
// Mark notes as spent.
|
|
let mut total_shielded_value_spent: u64 = 0;
|
|
|
|
//info!("Txid {} belongs to wallet", tx.txid);
|
|
|
|
for spend in &tx.shielded_spends {
|
|
let txid = nfs
|
|
.iter()
|
|
.find(|(nf, _, _)| &nf[..] == &spend.nf[..])
|
|
.unwrap()
|
|
.2;
|
|
let mut spent_note = txs
|
|
.get_mut(&txid)
|
|
.unwrap()
|
|
.notes
|
|
.iter_mut()
|
|
.find(|nd| &nd.nullifier[..] == &spend.nf[..])
|
|
.unwrap();
|
|
|
|
// Mark the note as spent, and remove the unconfirmed part of it
|
|
info!("Marked a note as spent");
|
|
spent_note.spent = Some(tx.txid);
|
|
spent_note.unconfirmed_spent = None::<TxId>;
|
|
|
|
total_shielded_value_spent += spent_note.note.value;
|
|
}
|
|
|
|
// Find the existing transaction entry, or create a new one.
|
|
if !txs.contains_key(&tx.txid) {
|
|
let tx_entry = WalletTx::new(block_data.height as i32, block.time as u64, &tx.txid);
|
|
txs.insert(tx.txid, tx_entry);
|
|
}
|
|
let tx_entry = txs.get_mut(&tx.txid).unwrap();
|
|
tx_entry.total_shielded_value_spent = total_shielded_value_spent;
|
|
|
|
// Save notes.
|
|
for output in tx.shielded_outputs
|
|
{
|
|
let new_note = SaplingNoteData::new(&self.zkeys.read().unwrap()[output.account].extfvk, output);
|
|
match LightWallet::note_address(self.config.hrp_sapling_address(), &new_note) {
|
|
Some(a) => {
|
|
// info!("Received sapling output to {}", a);
|
|
self.ensure_hd_zaddresses(&a);
|
|
},
|
|
None => {}
|
|
}
|
|
|
|
match tx_entry.notes.iter().find(|nd| nd.nullifier == new_note.nullifier) {
|
|
None => tx_entry.notes.push(new_note),
|
|
Some(_) => warn!("Tried to insert duplicate note for Tx {}", tx.txid)
|
|
};
|
|
}
|
|
}
|
|
|
|
{
|
|
let mut blks = self.blocks.write().unwrap();
|
|
|
|
// Store scanned data for this block.
|
|
blks.push(block_data);
|
|
|
|
// Trim the old blocks, keeping only as many as needed for a worst-case reorg (i.e. 101 blocks)
|
|
let len = blks.len();
|
|
if len > MAX_REORG + 1 {
|
|
let drain_first = len - (MAX_REORG+1);
|
|
blks.drain(..drain_first);
|
|
}
|
|
}
|
|
|
|
{
|
|
// Cleanup mempool tx after adding a block, to remove all txns that got mined
|
|
self.cleanup_mempool();
|
|
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<F> (
|
|
&self,
|
|
consensus_branch_id: u32,
|
|
spend_params: &[u8],
|
|
output_params: &[u8],
|
|
_transparent_only: bool,
|
|
tos: Vec<(&str, u64, Option<String>)>,
|
|
fee: &u64,
|
|
broadcast_fn: F
|
|
) -> Result<(String, Vec<u8>), String>
|
|
where F: Fn(Box<[u8]>) -> Result<String, String>
|
|
{
|
|
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::<Vec<_>>();
|
|
// 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::<u64>() 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<Vec<(address::RecipientAddress, Amount, Option<String>)>, 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();
|
|
// Select the candidate notes that are eligible to be spent
|
|
let notes: Vec<_> = self.txs.read().unwrap().iter()
|
|
.map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note)))
|
|
.flatten()
|
|
.filter_map(|(txid, note)| {
|
|
// Filter out notes that are already spent
|
|
if note.spent.is_some() || note.unconfirmed_spent.is_some() {
|
|
None
|
|
} else {
|
|
// Get the spending key for the selected fvk, if we have it
|
|
let extsk = self.zkeys.read().unwrap().iter()
|
|
.find(|zk| zk.extfvk == note.extfvk)
|
|
.and_then(|zk| zk.extsk.clone());
|
|
SpendableNote::from(txid, note, anchor_offset, &extsk)
|
|
}
|
|
})
|
|
.scan(0, |running_total, spendable| {
|
|
|
|
let value = spendable.note.value;
|
|
let ret = if *running_total < u64::from(target_value) {
|
|
Some(spendable)
|
|
} else {
|
|
None
|
|
};
|
|
*running_total = *running_total + value;
|
|
ret
|
|
})
|
|
.collect();
|
|
|
|
let mut builder = Builder::new(height);
|
|
|
|
// A note on t addresses
|
|
// Funds received by t-addresses can't be explicitly spent in 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::<HashMap<_,_>>();
|
|
|
|
// 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::<Result<Vec<_>, _>>()
|
|
.map_err(|e| format!("{}", e))?;
|
|
|
|
|
|
// Confirm we were able to select sufficient value
|
|
let selected_value = notes.iter().map(|selected| selected.note.value).sum::<u64>()
|
|
+ tinputs.iter().map::<u64, _>(|utxo| utxo.value.into()).sum::<u64>();
|
|
|
|
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
|
|
let mut txs = self.txs.write().unwrap();
|
|
for selected in notes {
|
|
let mut spent_note = txs.get_mut(&selected.txid).unwrap()
|
|
.notes.iter_mut()
|
|
.find(|nd| &nd.nullifier[..] == &selected.nullifier[..])
|
|
.unwrap();
|
|
spent_note.unconfirmed_spent = Some(tx.txid());
|
|
}
|
|
|
|
// Mark this utxo as unconfirmed spent
|
|
for utxo in tinputs {
|
|
let mut spent_utxo = txs.get_mut(&utxo.txid).unwrap().utxos.iter_mut()
|
|
.find(|u| utxo.txid == u.txid && utxo.output_index == u.output_index)
|
|
.unwrap();
|
|
spent_utxo.unconfirmed_spent = Some(tx.txid());
|
|
}
|
|
}
|
|
|
|
// Add this Tx to the mempool structure
|
|
{
|
|
let mut mempool_txs = self.mempool_txs.write().unwrap();
|
|
|
|
match mempool_txs.get_mut(&tx.txid()) {
|
|
None => {
|
|
// Collect the outgoing metadata
|
|
let outgoing_metadata = tos.iter().map(|(addr, amt, maybe_memo)| {
|
|
OutgoingTxMetadata {
|
|
address: addr.to_string(),
|
|
value: *amt,
|
|
memo: match maybe_memo {
|
|
None => Memo::default(),
|
|
Some(s) => {
|
|
// If the address is not a z-address, then drop the memo
|
|
if LightWallet::is_shielded_address(&addr.to_string(), &self.config) {
|
|
Memo::from_str(s).unwrap()
|
|
} else {
|
|
Memo::default()
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}).collect::<Vec<_>>();
|
|
|
|
// 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;
|