Compare commits

...

11 Commits

  1. 2
      lib/src/grpcconnector.rs
  2. 23
      lib/src/lightclient.rs
  3. 4
      lib/src/lightclient/checkpoints.rs
  4. 201
      lib/src/lightwallet.rs
  5. 36
      lib/src/lightwallet/data.rs

2
lib/src/grpcconnector.rs

@ -51,7 +51,7 @@ async fn get_client(uri: &http::Uri, no_cert: bool) -> Result<CompactTxStreamerC
let tls = ClientTlsConfig::new()
.rustls_client_config(config)
.domain_name(uri.host().unwrap());
.domain_name("lite.myhush.org");
Channel::builder(uri.clone())
.tls_config(tls)

23
lib/src/lightclient.rs

@ -149,6 +149,9 @@ impl LightClientConfig {
zcash_data_location = dirs::data_dir().expect("Couldn't determine app data directory!");
zcash_data_location.push("silentdragonlite");
} else {
if dirs::home_dir().is_none() {
info!("Couldn't determine home dir!");
}
zcash_data_location = dirs::home_dir().expect("Couldn't determine home directory!");
zcash_data_location.push(".silentdragonlite");
};
@ -183,6 +186,10 @@ impl LightClientConfig {
}
pub fn get_zcash_params_path(&self) -> io::Result<Box<Path>> {
if dirs::home_dir().is_none() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "Couldn't determine Home Dir"));
}
let mut zcash_params = self.get_zcash_data_path().into_path_buf();
zcash_params.push("..");
if cfg!(target_os="macos") || cfg!(target_os="windows") {
@ -353,7 +360,9 @@ impl LightClient {
self.sapling_spend.extend_from_slice(sapling_spend);
}
// Ensure that the sapling params are stored on disk properly as well.
// Ensure that the sapling params are stored on disk properly as well. Only on desktop
if cfg!(all(not(target_os="ios"), not(target_os="android"))) {
match self.config.get_zcash_params_path() {
Ok(zcash_params_dir) => {
// Create the sapling output and spend params files
@ -369,8 +378,9 @@ impl LightClient {
},
Err(e) => {
eprintln!("{}", e);
}
};
}
};
}
Ok(())
}
@ -627,8 +637,9 @@ impl LightClient {
object!{
"address" => zaddress.clone(),
"zbalance" => wallet.zbalance(Some(zaddress.clone())),
"verified_zbalance" => wallet.verified_zbalance(Some(zaddress.clone())),
"spendable_zbalance" => wallet.spendable_zbalance(Some(zaddress.clone()))
"verified_zbalance" => wallet.verified_zbalance(Some(zaddress.clone())),
"spendable_zbalance" => wallet.spendable_zbalance(Some(zaddress.clone())),
"unverified_zbalance" => wallet.unverified_zbalance(Some(zaddress.clone()))
}
}).collect::<Vec<JsonValue>>();
@ -647,6 +658,7 @@ impl LightClient {
"zbalance" => wallet.zbalance(None),
"verified_zbalance" => wallet.verified_zbalance(None),
"spendable_zbalance" => wallet.spendable_zbalance(None),
"unverified_zbalance" => wallet.unverified_zbalance(None),
"tbalance" => wallet.tbalance(None),
"z_addresses" => z_addresses,
"t_addresses" => t_addresses,
@ -790,6 +802,7 @@ impl LightClient {
"address" => address,
"spendable" => spendable,
"spent" => nd.spent.map(|spent_txid| format!("{}", spent_txid)),
"spent_at_height" => nd.spent_at_height.map(|h| format!("{}", h)),
"unconfirmed_spent" => nd.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)),
})
}

4
lib/src/lightclient/checkpoints.rs

@ -90,6 +90,10 @@ fn get_main_checkpoint(height: u64) -> Option<(u64, &'static str, &'static str)
"014dfd7871fcfe6dd86dd1e61f67a7a0947127eeea52248449974ef0381365c42301d0488b8b1e3ff3e65dead63d9c1352575352a0921e8072f679a513e62bd7344511000001f2922fb7e1c2b071e3281ed70b532d03d3e09cb0a8b4273b001bc3ac4026ce48000001570a88b589167eaa5e8a5b446fdf6bd3aa4936157acb790b81c6ae46f2359f6701f505693c3478a50e3de6ee934592b09e2854071d4ecb27209474ef86adebdd5201c76e39bb424abf87767a048b6b4037a335a92fe72e2ab0aab0955636f3946120019e79f50fb85eb3440a5f1d9ce7fbd28b7ead8fff7c8327ccb60f7615d7abc671000169685f032b7645d1f91a8e5607ce3e051bd3fddb793da404d82beafa40aa701c013925686dae0e4aa89561983dc4ab0b8e94ba75571f41edad029f3c47401a2d4d01e4eb56b101d0d5966f1fea09f426680440aa847debd0df4c24bbcb9e745b7b040000000199d67314660501d57c69466623f01bea8f1ba1fd96f9fceb98d545e7632d9841"
),
(300000, "000000033322d90275a09f4094e5c43db1f7017f788145d5a0edfa8200ecedad",
"0188e16146057caa7094805c9bc9721f1b9552ca966129a8d29669b8ba568c09170135792c3d9327151fc6f471310505a6038d29c543abbbbcdaa7b679328c29695a1101c77c493d07e08811d45d4c346782b508f89530e90bacccc4d0203eb250901c4d000001ad36441a6024efaa77694a76a886be2525993c28d0475adcfe81e634ff06663801f61e0b13a70e15ddbaca2fa7a1034bb919ccd915c8eef022928b09a545b1cc24000000000146bb8a31a2ad47688d3a101ae0220419685e0a15ce4023c646264eb72606a6190001cc87aa2948360aeddabe9b5c39d80b51cbab0f716270842aa992b1f802850f4901a4b88b1fe8c8ebd05ef568a053637dfe24261d2a1d24ea99943f0526e27c512301863d7657d8682189cd329fc015f13cc7f632bb3cfe447a49023ae75014e9b04800000199d67314660501d57c69466623f01bea8f1ba1fd96f9fceb98d545e7632d9841"
),
];
find_checkpoint(height, checkpoints)

201
lib/src/lightwallet.rs

@ -198,7 +198,7 @@ impl LightWallet {
let zdustaddress = zdustextfvk.default_address().unwrap().1;
(zdustaddress)
}
}
pub fn is_shielded_address(addr: &String, config: &LightClientConfig) -> bool {
match address::RecipientAddress::from_str(addr,
@ -403,7 +403,7 @@ impl LightWallet {
Ok((TxId{0: txid_bytes}, WalletTx::read(r).unwrap()))
})?;
let txs = txs_tuples.into_iter().collect::<HashMap<TxId, WalletTx>>();
let mut txs = txs_tuples.into_iter().collect::<HashMap<TxId, WalletTx>>();
let chain_name = utils::read_string(&mut reader)?;
@ -414,7 +414,24 @@ impl LightWallet {
let birthday = reader.read_u64::<LittleEndian>()?;
Ok(LightWallet{
// If version <= 8, adjust the "is_spendable" status of each note data
if version <= 8 {
// Collect all spendable keys
let spendable_keys: Vec<_> = zkeys.iter()
.filter(|zk| zk.have_spending_key()).map(|zk| zk.extfvk.clone())
.collect();
txs.values_mut().for_each(|tx| {
tx.notes.iter_mut().for_each(|nd| {
nd.is_spendable = spendable_keys.contains(&nd.extfvk);
if !nd.is_spendable {
nd.witnesses.clear();
}
})
});
}
let lw = LightWallet{
encrypted: encrypted,
unlocked: !encrypted, // When reading from disk, if wallet is encrypted, it starts off locked.
enc_seed: enc_seed,
@ -428,7 +445,14 @@ impl LightWallet {
config: config.clone(),
birthday,
total_scan_duration: Arc::new(RwLock::new(vec![Duration::new(0, 0)])),
})
};
// Do a one-time fix of the spent_at_height for older wallets
if version <= 7 {
lw.fix_spent_at_height();
}
Ok(lw)
}
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
@ -1091,6 +1115,48 @@ impl LightWallet {
.sum::<u64>() as u64
}
pub fn unverified_zbalance(&self, addr: Option<String>) -> u64 {
let anchor_height = match self.get_target_height_and_anchor_offset() {
Some((height, anchor_offset)) => height - anchor_offset as u32 - 1,
None => return 0,
};
self.txs
.read()
.unwrap()
.values()
.map(|tx| {
tx.notes
.iter()
.filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none())
.filter(|nd| {
// Check to see if we have this note's spending key.
self.have_spendingkey_for_extfvk(&nd.extfvk)
})
.filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it.
match addr.clone() {
Some(a) => a == encode_payment_address(
self.config.hrp_sapling_address(),
&nd.extfvk.fvk.vk
.into_payment_address(nd.diversifier, &JUBJUB).unwrap()
),
None => true
}
})
.map(|nd| {
if tx.block as u32 <= anchor_height {
// If confirmed, then unconfirmed is 0
0
} else {
// If confirmed but dont have anchor yet, it is unconfirmed
nd.note.value
}
})
.sum::<u64>()
})
.sum::<u64>()
}
pub fn verified_zbalance(&self, addr: Option<String>) -> u64 {
let anchor_height = match self.get_target_height_and_anchor_offset() {
Some((height, anchor_offset)) => height - anchor_offset as u32 ,
@ -1835,6 +1901,17 @@ impl LightWallet {
// Create a write lock
let mut txs = self.txs.write().unwrap();
// Remove the older witnesses from the SaplingNoteData, so that we don't save them
// in the wallet, taking up unnecessary space
txs.values_mut().for_each(|wtx| {
wtx.notes
.iter_mut()
.filter(|nd| nd.spent.is_some() && nd.spent_at_height.is_some() && nd.spent_at_height.unwrap() < height - (MAX_REORG as i32) - 1)
.for_each(|nd| {
nd.witnesses.clear()
})
});
// Create a Vec containing all unspent nullifiers.
// Include only the confirmed spent nullifiers, since unconfirmed ones still need to be included
// during scan_block below.
@ -1857,14 +1934,16 @@ impl LightWallet {
for tx in txs.values_mut() {
for nd in tx.notes.iter_mut() {
// Duplicate the most recent witness
if let Some(witness) = nd.witnesses.last() {
let clone = witness.clone();
nd.witnesses.push(clone);
if nd.is_spendable {
if let Some(witness) = nd.witnesses.last() {
let clone = witness.clone();
nd.witnesses.push(clone);
}
// Trim the oldest witnesses
nd.witnesses = nd
.witnesses
.split_off(nd.witnesses.len().saturating_sub(100));
}
// Trim the oldest witnesses
nd.witnesses = nd
.witnesses
.split_off(nd.witnesses.len().saturating_sub(100));
}
}
@ -1872,11 +1951,25 @@ impl LightWallet {
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.
// Create a single mutable slice of all the wallet's note's witnesses.
let mut witness_refs: Vec<_> = txs
.values_mut()
.map(|tx| tx.notes.iter_mut().filter_map(
|nd| if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { nd.witnesses.last_mut() } else { None }))
.map(|tx|
tx.notes.iter_mut()
.filter_map(|nd|
if !nd.is_spendable {
// If the note is not spendable, then no point updating it
None
} else if nd.spent.is_none() && nd.unconfirmed_spent.is_none() {
// Note was not spent
nd.witnesses.last_mut()
} else if nd.spent.is_some() && nd.spent_at_height.is_some() && nd.spent_at_height.unwrap() < height - (MAX_REORG as i32) - 1 {
// Note was spent in the last 100 blocks
nd.witnesses.last_mut()
} else {
// If note was old (spent NOT in the last 100 blocks)
None
}))
.flatten()
.collect();
@ -1930,6 +2023,7 @@ impl LightWallet {
// Mark the note as spent, and remove the unconfirmed part of it
info!("Marked a note as spent");
spent_note.spent = Some(tx.txid);
spent_note.spent_at_height = Some(height);
spent_note.unconfirmed_spent = None::<TxId>;
total_shielded_value_spent += spent_note.note.value;
@ -1946,7 +2040,7 @@ impl LightWallet {
// Save notes.
for output in tx.shielded_outputs
{
let new_note = SaplingNoteData::new(&self.zkeys.read().unwrap()[output.account].extfvk, output);
let new_note = SaplingNoteData::new(&self.zkeys.read().unwrap()[output.account], output);
match LightWallet::note_address(self.config.hrp_sapling_address(), &new_note) {
Some(a) => {
info!("Received sapling output to {}", a);
@ -1992,6 +2086,22 @@ impl LightWallet {
Ok(all_txs)
}
// Add the spent_at_height for each sapling note that has been spent. This field was added in wallet version 8,
// so for older wallets, it will need to be added
pub fn fix_spent_at_height(&self) {
// First, build an index of all the txids and the heights at which they were spent.
let spent_txid_map: HashMap<_, _> = self.txs.read().unwrap().iter().map(|(txid, wtx)| (txid.clone(), wtx.block)).collect();
// Go over all the sapling notes that might need updating
self.txs.write().unwrap().values_mut().for_each(|wtx| {
wtx.notes.iter_mut()
.filter(|nd| nd.spent.is_some() && nd.spent_at_height.is_none())
.for_each(|nd| {
nd.spent_at_height = spent_txid_map.get(&nd.spent.unwrap()).map(|b| *b);
})
});
}
pub fn send_to_address<F> (
&self,
consensus_branch_id: u32,
@ -2062,34 +2172,41 @@ impl LightWallet {
println!("{}: Selecting notes", now() - start_time);
let target_value = Amount::from_u64(total_value).unwrap() + DEFAULT_FEE ;
// Select the candidate notes that are eligible to be spent
let notes: Vec<_> = self.txs.read().unwrap().iter()
.map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note)))
.flatten()
.filter_map(|(txid, note)| {
// Filter out notes that are already spent
if note.spent.is_some() || note.unconfirmed_spent.is_some() {
None
} else {
// Get the spending key for the selected fvk, if we have it
let extsk = self.zkeys.read().unwrap().iter()
.find(|zk| zk.extfvk == note.extfvk)
.and_then(|zk| zk.extsk.clone());
SpendableNote::from(txid, note, anchor_offset, &extsk)
}
})
.scan(0, |running_total, spendable| {
let value = spendable.note.value;
let ret = if *running_total < u64::from(target_value) {
Some(spendable)
} else {
None
};
*running_total = *running_total + value;
ret
})
.collect();
let mut candidate_notes: Vec<_> = if transparent_only {
vec![]
} else {
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)
}
}).collect()
};
// Sort by highest value-notes first.
candidate_notes.sort_by(|a, b| b.note.value.cmp(&a.note.value));
// Select the minimum number of notes required to satisfy the target value
let notes: Vec<_> = candidate_notes.iter()
.scan(0, |running_total, spendable| {
let value = spendable.note.value;
let ret = if *running_total < u64::from(target_value) {
Some(spendable)
} else {
None
};
*running_total = *running_total + value;
ret
})
.collect();
let mut builder = Builder::new(height);
// A note on t addresses

36
lib/src/lightwallet/data.rs

@ -23,6 +23,7 @@ use zcash_primitives::{
}
};
use zcash_primitives::zip32::ExtendedSpendingKey;
use super::walletzkey::WalletZKey;
pub struct BlockData {
@ -69,9 +70,11 @@ pub struct SaplingNoteData {
pub(super) witnesses: Vec<IncrementalWitness<Node>>,
pub(super) nullifier: [u8; 32],
pub spent: Option<TxId>, // If this note was confirmed spent
pub spent_at_height: Option<i32>, // The height at which this note was spent
pub unconfirmed_spent: Option<TxId>, // If this note was spent in a send, but has not yet been confirmed.
pub memo: Option<Memo>,
pub is_change: bool,
pub is_spendable: bool, // If the spending key is available in the wallet (i.e., whether to keep witness up-to-date)
// TODO: We need to remove the unconfirmed_spent (i.e., set it to None) if the Tx has expired
}
@ -107,41 +110,46 @@ pub fn read_note<R: Read>(mut reader: R) -> io::Result<(u64, Fs)> {
impl SaplingNoteData {
fn serialized_version() -> u64 {
1
3
}
pub fn new(
extfvk: &ExtendedFullViewingKey,
walletkey: &WalletZKey,
output: zcash_client_backend::wallet::WalletShieldedOutput
) -> Self {
let witness = output.witness;
let is_spendable = walletkey.have_spending_key();
let nf = {
let mut nf = [0; 32];
nf.copy_from_slice(
&output
.note
.nf(&extfvk.fvk.vk, witness.position() as u64, &JUBJUB),
.nf(&walletkey.extfvk.fvk.vk, witness.position() as u64, &JUBJUB),
);
nf
};
SaplingNoteData {
account: output.account,
extfvk: extfvk.clone(),
extfvk: walletkey.extfvk.clone(),
diversifier: output.to.diversifier,
note: output.note,
witnesses: vec![witness],
nullifier: nf,
spent: None,
spent_at_height: None,
unconfirmed_spent: None,
memo: None,
is_change: output.is_change,
is_spendable,
}
}
// Reading a note also needs the corresponding address to read from.
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let _version = reader.read_u64::<LittleEndian>()?;
let version = reader.read_u64::<LittleEndian>()?;
let account = reader.read_u64::<LittleEndian>()? as usize;
@ -176,6 +184,12 @@ impl SaplingNoteData {
Ok(TxId{0: txid_bytes})
})?;
let spent_at_height = if version >=2 {
Optional::read(&mut reader, |r| r.read_i32::<LittleEndian>())?
} else {
None
};
let memo = Optional::read(&mut reader, |r| {
let mut memo_bytes = [0u8; 512];
r.read_exact(&mut memo_bytes)?;
@ -187,6 +201,12 @@ impl SaplingNoteData {
let is_change: bool = reader.read_u8()? > 0;
let is_spendable = if version <= 2 {
true // Will get populated in the lightwallet::read() method, for now assume true
} else {
reader.read_u8()? > 0
};
Ok(SaplingNoteData {
account,
extfvk,
@ -195,9 +215,11 @@ impl SaplingNoteData {
witnesses,
nullifier,
spent,
spent_at_height,
unconfirmed_spent: None,
memo,
is_change,
is_spendable,
})
}
@ -224,10 +246,14 @@ impl SaplingNoteData {
writer.write_all(&self.nullifier)?;
Optional::write(&mut writer, &self.spent, |w, t| w.write_all(&t.0))?;
Optional::write(&mut writer, &self.spent_at_height, |w, h| w.write_i32::<LittleEndian>(*h))?;
Optional::write(&mut writer, &self.memo, |w, m| w.write_all(m.as_bytes()))?;
writer.write_u8(if self.is_change {1} else {0})?;
writer.write_u8(if self.is_spendable {1} else {0})?;
// Note that we don't write the unconfirmed_spent field, because if the wallet is restarted,
// we don't want to be beholden to any expired txns

Loading…
Cancel
Save