When the mempool monitor saw a transaction first, its wallet entry kept the monitor's height (often 0) after the transaction was mined, because scanning the block reused the entry without updating its block. A later reorg between the two heights then left a phantom spend of our own send and a change note with an empty witness list until a rescan. Scanning a block now sets the entry's block to the block height. This applies to every monitor-first transaction, including payments received from others: their reported block height, the verified/spendable timing that depends on it, and the wallet birthday now use the real mined height instead of the monitor's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3260 lines
132 KiB
Rust
3260 lines
132 KiB
Rust
use std::convert::TryInto;
|
|
use std::io::{Error};
|
|
use rand::{RngCore, rngs::OsRng};
|
|
|
|
use ff::{Field, PrimeField, PrimeFieldRepr};
|
|
use pairing::bls12_381::Bls12;
|
|
use protobuf::{Message, UnknownFields, CachedSize, RepeatedField};
|
|
use zcash_client_backend::{encoding::encode_payment_address,
|
|
proto::compact_formats::{
|
|
CompactBlock, CompactOutput, CompactSpend, CompactTx,
|
|
}
|
|
};
|
|
use zcash_primitives::{
|
|
block::BlockHash,
|
|
jubjub::fs::Fs,
|
|
note_encryption::{Memo, SaplingNoteEncryption},
|
|
primitives::{Note, PaymentAddress},
|
|
legacy::{Script, TransparentAddress,},
|
|
transaction::{
|
|
TxId, Transaction, TransactionData,
|
|
components::{TxOut, TxIn, OutPoint, Amount,},
|
|
components::amount::DEFAULT_FEE,
|
|
},
|
|
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
|
JUBJUB,
|
|
};
|
|
|
|
use sha2::{Sha256, Digest};
|
|
|
|
use super::LightWallet;
|
|
use super::LightClientConfig;
|
|
use secp256k1::{Secp256k1, key::PublicKey, key::SecretKey};
|
|
use crate::SaplingParams;
|
|
|
|
fn get_sapling_params() -> Result<(Vec<u8>, Vec<u8>), Error> {
|
|
// Read Sapling Params
|
|
let mut sapling_output = vec![];
|
|
sapling_output.extend_from_slice(SaplingParams::get("sapling-output.params").unwrap().as_ref());
|
|
println!("Read output {}", sapling_output.len());
|
|
|
|
let mut sapling_spend = vec![];
|
|
sapling_spend.extend_from_slice(SaplingParams::get("sapling-spend.params").unwrap().as_ref());
|
|
println!("Read output {}", sapling_spend.len());
|
|
|
|
Ok((sapling_spend, sapling_output))
|
|
}
|
|
|
|
// send_to_address now takes the fee explicitly; the tests send with the default fee.
|
|
fn default_fee() -> u64 {
|
|
u64::from(DEFAULT_FEE)
|
|
}
|
|
|
|
struct FakeCompactBlock {
|
|
block: CompactBlock,
|
|
}
|
|
|
|
impl FakeCompactBlock {
|
|
fn new(height: i32, prev_hash: BlockHash) -> Self {
|
|
// Create a fake Note for the account
|
|
let mut rng = OsRng;
|
|
|
|
let mut cb = CompactBlock::new();
|
|
|
|
cb.set_height(height as u64);
|
|
cb.hash.resize(32, 0);
|
|
rng.fill_bytes(&mut cb.hash);
|
|
|
|
cb.prevHash.extend_from_slice(&prev_hash.0);
|
|
|
|
FakeCompactBlock { block: cb }
|
|
}
|
|
|
|
fn as_bytes(&self) -> Vec<u8> {
|
|
self.block.write_to_bytes().unwrap()
|
|
}
|
|
|
|
fn hash(&self) -> BlockHash {
|
|
BlockHash(self.block.hash[..].try_into().unwrap())
|
|
}
|
|
|
|
fn tx_to_compact_tx(tx: &Transaction, index: u64) -> CompactTx {
|
|
let spends = tx.shielded_spends.iter().map(|s| {
|
|
let mut c_spend = CompactSpend::default();
|
|
c_spend.set_nf(s.nullifier.to_vec());
|
|
|
|
c_spend
|
|
}).collect::<Vec<CompactSpend>>();
|
|
|
|
let outputs = tx.shielded_outputs.iter().map(|o| {
|
|
let mut c_out = CompactOutput::default();
|
|
|
|
let mut cmu_bytes = vec![];
|
|
o.cmu.into_repr().write_le(&mut cmu_bytes).unwrap();
|
|
|
|
let mut epk_bytes = vec![];
|
|
o.ephemeral_key.write(&mut epk_bytes).unwrap();
|
|
|
|
c_out.set_cmu(cmu_bytes);
|
|
c_out.set_epk(epk_bytes);
|
|
c_out.set_ciphertext(o.enc_ciphertext[0..52].to_vec());
|
|
|
|
c_out
|
|
}).collect::<Vec<CompactOutput>>();
|
|
|
|
CompactTx {
|
|
index,
|
|
hash: tx.txid().0.to_vec(),
|
|
fee: 0, // TODO: Get Fee
|
|
spends: RepeatedField::from_vec(spends),
|
|
outputs: RepeatedField::from_vec(outputs),
|
|
unknown_fields: UnknownFields::default(),
|
|
cached_size: CachedSize::default(),
|
|
}
|
|
}
|
|
|
|
// Convert the transaction into a CompactTx and add it to this block
|
|
fn add_tx(&mut self, tx: &Transaction) {
|
|
let ctx = FakeCompactBlock::tx_to_compact_tx(&tx, self.block.vtx.len() as u64);
|
|
self.block.vtx.push(ctx);
|
|
}
|
|
|
|
// Add a new tx into the block, paying the given address the amount.
|
|
// Returns the nullifier of the new note.
|
|
fn add_tx_paying(&mut self, extfvk: ExtendedFullViewingKey, value: u64)
|
|
-> (Vec<u8>, TxId) {
|
|
let to = extfvk.default_address().unwrap().1;
|
|
self.add_tx_paying_to(extfvk, to, value)
|
|
}
|
|
|
|
// add_tx_paying with an explicit recipient (e.g. a diversified address of `extfvk`).
|
|
fn add_tx_paying_to(&mut self, extfvk: ExtendedFullViewingKey, to: PaymentAddress<Bls12>, value: u64)
|
|
-> (Vec<u8>, TxId) {
|
|
let value = Amount::from_u64(value).unwrap();
|
|
|
|
// Create a fake Note for the account
|
|
let mut rng = OsRng;
|
|
let note = Note {
|
|
g_d: to.diversifier.g_d::<Bls12>(&JUBJUB).unwrap(),
|
|
pk_d: to.pk_d.clone(),
|
|
value: value.into(),
|
|
r: Fs::random(&mut rng),
|
|
};
|
|
let encryptor = SaplingNoteEncryption::new(
|
|
extfvk.fvk.ovk,
|
|
note.clone(),
|
|
to.clone(),
|
|
Memo::default(),
|
|
&mut rng,
|
|
);
|
|
let mut cmu = vec![];
|
|
note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap();
|
|
let mut epk = vec![];
|
|
encryptor.epk().write(&mut epk).unwrap();
|
|
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
|
|
|
// Create a fake CompactBlock containing the note
|
|
let mut cout = CompactOutput::new();
|
|
cout.set_cmu(cmu);
|
|
cout.set_epk(epk);
|
|
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
|
|
let mut ctx = CompactTx::new();
|
|
let mut txid = vec![0; 32];
|
|
rng.fill_bytes(&mut txid);
|
|
ctx.set_hash(txid.clone());
|
|
ctx.outputs.push(cout);
|
|
|
|
self.block.vtx.push(ctx);
|
|
(note.nf(&extfvk.fvk.vk, 0, &JUBJUB), TxId(txid[..].try_into().unwrap()))
|
|
}
|
|
|
|
fn add_tx_spending(&mut self,
|
|
(nf, in_value): (Vec<u8>, u64),
|
|
extfvk: ExtendedFullViewingKey,
|
|
to: PaymentAddress<Bls12>,
|
|
value: u64) -> TxId {
|
|
let mut rng = OsRng;
|
|
|
|
let in_value = Amount::from_u64(in_value).unwrap();
|
|
let value = Amount::from_u64(value).unwrap();
|
|
|
|
// Create a fake CompactBlock containing the note
|
|
let mut cspend = CompactSpend::new();
|
|
cspend.set_nf(nf);
|
|
let mut ctx = CompactTx::new();
|
|
let mut txid = vec![0; 32];
|
|
rng.fill_bytes(&mut txid);
|
|
ctx.set_hash(txid.clone());
|
|
ctx.spends.push(cspend);
|
|
|
|
// Create a fake Note for the payment
|
|
ctx.outputs.push({
|
|
let note = Note {
|
|
g_d: to.diversifier.g_d::<Bls12>(&JUBJUB).unwrap(),
|
|
pk_d: to.pk_d.clone(),
|
|
value: value.into(),
|
|
r: Fs::random(&mut rng),
|
|
};
|
|
let encryptor = SaplingNoteEncryption::new(
|
|
extfvk.fvk.ovk,
|
|
note.clone(),
|
|
to,
|
|
Memo::default(),
|
|
&mut rng,
|
|
);
|
|
let mut cmu = vec![];
|
|
note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap();
|
|
let mut epk = vec![];
|
|
encryptor.epk().write(&mut epk).unwrap();
|
|
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
|
|
|
let mut cout = CompactOutput::new();
|
|
cout.set_cmu(cmu);
|
|
cout.set_epk(epk);
|
|
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
|
|
cout
|
|
});
|
|
|
|
// Create a fake Note for the change
|
|
ctx.outputs.push({
|
|
let change_addr = extfvk.default_address().unwrap().1;
|
|
let note = Note {
|
|
g_d: change_addr.diversifier.g_d::<Bls12>(&JUBJUB).unwrap(),
|
|
pk_d: change_addr.pk_d.clone(),
|
|
value: (in_value - value).into(),
|
|
r: Fs::random(&mut rng),
|
|
};
|
|
let encryptor = SaplingNoteEncryption::new(
|
|
extfvk.fvk.ovk,
|
|
note.clone(),
|
|
change_addr,
|
|
Memo::default(),
|
|
&mut rng,
|
|
);
|
|
let mut cmu = vec![];
|
|
note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap();
|
|
let mut epk = vec![];
|
|
encryptor.epk().write(&mut epk).unwrap();
|
|
let enc_ciphertext = encryptor.encrypt_note_plaintext();
|
|
|
|
let mut cout = CompactOutput::new();
|
|
cout.set_cmu(cmu);
|
|
cout.set_epk(epk);
|
|
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
|
|
cout
|
|
});
|
|
|
|
self.block.vtx.push(ctx);
|
|
|
|
TxId(txid[..].try_into().unwrap())
|
|
}
|
|
}
|
|
|
|
struct FakeTransaction {
|
|
tx: Transaction,
|
|
}
|
|
|
|
impl FakeTransaction {
|
|
// New FakeTransaction with random txid
|
|
fn new<R: RngCore>(rng: &mut R) -> Self {
|
|
let mut txid = [0u8; 32];
|
|
rng.fill_bytes(&mut txid);
|
|
FakeTransaction::new_with_txid(TxId(txid))
|
|
}
|
|
|
|
fn new_with_txid(txid: TxId) -> Self {
|
|
FakeTransaction {
|
|
tx: Transaction {
|
|
txid,
|
|
data: TransactionData::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_tx(&self) -> &Transaction {
|
|
&self.tx
|
|
}
|
|
|
|
fn add_t_output(&mut self, pk: &PublicKey, value: u64) {
|
|
let mut hash160 = ripemd160::Ripemd160::new();
|
|
hash160.input(Sha256::digest(&pk.serialize()[..].to_vec()));
|
|
|
|
let taddr_bytes = hash160.result();
|
|
|
|
self.tx.data.vout.push(TxOut {
|
|
value: Amount::from_u64(value).unwrap(),
|
|
script_pubkey: TransparentAddress::PublicKey(taddr_bytes.try_into().unwrap()).script(),
|
|
});
|
|
}
|
|
|
|
fn add_t_input(&mut self, txid: TxId, n: u32) {
|
|
self.tx.data.vin.push(TxIn {
|
|
prevout: OutPoint{
|
|
hash: txid.0,
|
|
n
|
|
},
|
|
script_sig: Script{0: vec![]},
|
|
sequence: 0,
|
|
});
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_z_balances() {
|
|
let wallet = LightWallet::new(None, &get_test_config(), 0, 0).unwrap();
|
|
|
|
const AMOUNT1:u64 = 5;
|
|
// Address is encoded in bech32
|
|
let address = Some(encode_payment_address(wallet.config.hrp_sapling_address(),
|
|
&wallet.zkeys.read().unwrap()[0].extfvk.default_address().unwrap().1));
|
|
|
|
let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32]));
|
|
cb1.add_tx_paying(wallet.zkeys.read().unwrap()[0].extfvk.clone(), AMOUNT1);
|
|
|
|
// Make sure that the intial state is empty
|
|
assert_eq!(wallet.txs.read().unwrap().len(), 0);
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 0);
|
|
assert_eq!(wallet.zbalance(None), 0);
|
|
assert_eq!(wallet.zbalance(address.clone()), 0);
|
|
|
|
wallet.scan_block(&cb1.as_bytes()).unwrap();
|
|
|
|
assert_eq!(wallet.txs.read().unwrap().len(), 1);
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 1);
|
|
assert_eq!(wallet.zbalance(None), AMOUNT1);
|
|
assert_eq!(wallet.zbalance(address.clone()), AMOUNT1);
|
|
|
|
const AMOUNT2:u64 = 10;
|
|
|
|
// Add a second block
|
|
let mut cb2 = FakeCompactBlock::new(1, cb1.hash());
|
|
cb2.add_tx_paying(wallet.zkeys.read().unwrap()[0].extfvk.clone(), AMOUNT2);
|
|
|
|
wallet.scan_block(&cb2.as_bytes()).unwrap();
|
|
|
|
assert_eq!(wallet.txs.read().unwrap().len(), 2);
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 2);
|
|
assert_eq!(wallet.zbalance(None), AMOUNT1 + AMOUNT2);
|
|
assert_eq!(wallet.zbalance(address.clone()), AMOUNT1 + AMOUNT2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_z_change_balances() {
|
|
let wallet = LightWallet::new(None, &get_test_config(), 0, 0).unwrap();
|
|
|
|
// First, add an incoming transaction
|
|
const AMOUNT1:u64 = 5;
|
|
|
|
let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32]));
|
|
let (nf1, txid1) = cb1.add_tx_paying(wallet.zkeys.read().unwrap()[0].extfvk.clone(), AMOUNT1);
|
|
|
|
wallet.scan_block(&cb1.as_bytes()).unwrap();
|
|
|
|
assert_eq!(wallet.txs.read().unwrap().len(), 1);
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 1);
|
|
assert_eq!(wallet.zbalance(None), AMOUNT1);
|
|
|
|
const AMOUNT2:u64 = 2;
|
|
|
|
// Add a second block, spending the first note
|
|
let addr2 = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[0u8; 32]))
|
|
.default_address().unwrap().1;
|
|
let mut cb2 = FakeCompactBlock::new(1, cb1.hash());
|
|
let txid2 = cb2.add_tx_spending((nf1, AMOUNT1), wallet.zkeys.read().unwrap()[0].extfvk.clone(), addr2, AMOUNT2);
|
|
wallet.scan_block(&cb2.as_bytes()).unwrap();
|
|
|
|
// Now, the original note should be spent and there should be a change
|
|
assert_eq!(wallet.zbalance(None), AMOUNT1 - AMOUNT2);
|
|
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// Old note was spent
|
|
assert_eq!(txs[&txid1].txid, txid1);
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].spent.unwrap(), txid2);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].notes[0].is_change, false);
|
|
|
|
// new note is not spent
|
|
assert_eq!(txs[&txid2].txid, txid2);
|
|
assert_eq!(txs[&txid2].notes.len(), 1);
|
|
assert_eq!(txs[&txid2].notes[0].spent, None);
|
|
assert_eq!(txs[&txid2].notes[0].note.value, AMOUNT1 - AMOUNT2);
|
|
assert_eq!(txs[&txid2].notes[0].is_change, true);
|
|
assert_eq!(txs[&txid2].total_shielded_value_spent, AMOUNT1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_t_receive_spend() {
|
|
let mut rng = OsRng;
|
|
let secp = Secp256k1::new();
|
|
|
|
let wallet = LightWallet::new(None, &get_test_config(), 0, 0).unwrap();
|
|
|
|
let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
|
|
const AMOUNT1: u64 = 20;
|
|
|
|
let mut tx = FakeTransaction::new(&mut rng);
|
|
tx.add_t_output(&pk, AMOUNT1);
|
|
let txid1 = tx.get_tx().txid();
|
|
|
|
wallet.scan_full_tx(&tx.get_tx(), 100, 0); // Pretend it is at height 100
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// Now make sure the t addr was recieved
|
|
assert_eq!(txs.len(), 1);
|
|
assert_eq!(txs[&txid1].utxos.len(), 1);
|
|
assert_eq!(txs[&txid1].utxos[0].address, taddr);
|
|
assert_eq!(txs[&txid1].utxos[0].txid, txid1);
|
|
assert_eq!(txs[&txid1].utxos[0].output_index, 0);
|
|
assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].utxos[0].height, 100);
|
|
assert_eq!(txs[&txid1].utxos[0].spent, None);
|
|
assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None);
|
|
|
|
assert_eq!(wallet.tbalance(None), AMOUNT1);
|
|
assert_eq!(wallet.tbalance(Some(taddr)), AMOUNT1);
|
|
}
|
|
|
|
// Create a new Tx, spending this taddr
|
|
let mut tx = FakeTransaction::new(&mut rng);
|
|
tx.add_t_input(txid1, 0);
|
|
let txid2 = tx.get_tx().txid();
|
|
|
|
wallet.scan_full_tx(&tx.get_tx(), 101, 0); // Pretent it is at height 101
|
|
|
|
{
|
|
// Make sure the txid was spent
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// Old utxo, that should be spent now
|
|
assert_eq!(txs.len(), 2);
|
|
assert_eq!(txs[&txid1].utxos.len(), 1);
|
|
assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].utxos[0].spent, Some(txid2));
|
|
assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None);
|
|
|
|
assert_eq!(txs[&txid2].block, 101); // The second TxId is at block 101
|
|
assert_eq!(txs[&txid2].utxos.len(), 0); // The second TxId has no UTXOs
|
|
assert_eq!(txs[&txid2].total_transparent_value_spent, AMOUNT1);
|
|
|
|
// Make sure there is no t-ZEC left
|
|
assert_eq!(wallet.tbalance(None), 0);
|
|
}
|
|
}
|
|
|
|
|
|
#[test]
|
|
/// This test spends and receives t addresses among non-wallet t addresses to make sure that
|
|
/// we're detecting and spending only our t addrs.
|
|
fn test_t_receive_spend_among_tadds() {
|
|
let mut rng = OsRng;
|
|
let secp = Secp256k1::new();
|
|
|
|
let wallet = LightWallet::new(None, &get_test_config(), 0, 0).unwrap();
|
|
|
|
let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
|
|
let non_wallet_sk = &SecretKey::from_slice(&[1u8; 32]).unwrap();
|
|
let non_wallet_pk = PublicKey::from_secret_key(&secp, &non_wallet_sk);
|
|
|
|
const AMOUNT1: u64 = 30;
|
|
|
|
let mut tx = FakeTransaction::new(&mut rng);
|
|
// Add a non-wallet output
|
|
tx.add_t_output(&non_wallet_pk, 20);
|
|
tx.add_t_output(&pk, AMOUNT1); // Our wallet t output
|
|
tx.add_t_output(&non_wallet_pk, 25);
|
|
let txid1 = tx.get_tx().txid();
|
|
|
|
wallet.scan_full_tx(&tx.get_tx(), 100, 0); // Pretend it is at height 100
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// Now make sure the t addr was received
|
|
assert_eq!(txs.len(), 1);
|
|
assert_eq!(txs[&txid1].utxos.len(), 1);
|
|
assert_eq!(txs[&txid1].utxos[0].address, taddr);
|
|
assert_eq!(txs[&txid1].utxos[0].txid, txid1);
|
|
assert_eq!(txs[&txid1].utxos[0].output_index, 1);
|
|
assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].utxos[0].height, 100);
|
|
assert_eq!(txs[&txid1].utxos[0].spent, None);
|
|
assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None);
|
|
|
|
assert_eq!(wallet.tbalance(None), AMOUNT1);
|
|
assert_eq!(wallet.tbalance(Some(taddr)), AMOUNT1);
|
|
}
|
|
|
|
// Create a new Tx, spending this taddr
|
|
let mut tx = FakeTransaction::new(&mut rng);
|
|
tx.add_t_input(txid1, 1); // Ours was at position 1 in the input tx
|
|
let txid2 = tx.get_tx().txid();
|
|
|
|
wallet.scan_full_tx(&tx.get_tx(), 101, 0); // Pretent it is at height 101
|
|
|
|
{
|
|
// Make sure the txid was spent
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// Old utxo, that should be spent now
|
|
assert_eq!(txs.len(), 2);
|
|
assert_eq!(txs[&txid1].utxos.len(), 1);
|
|
assert_eq!(txs[&txid1].utxos[0].value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].utxos[0].spent, Some(txid2));
|
|
assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None);
|
|
|
|
assert_eq!(txs[&txid2].block, 101); // The second TxId is at block 101
|
|
assert_eq!(txs[&txid2].utxos.len(), 0); // The second TxId has no UTXOs
|
|
assert_eq!(txs[&txid2].total_transparent_value_spent, AMOUNT1);
|
|
|
|
// Make sure there is no t-ZEC left
|
|
assert_eq!(wallet.tbalance(None), 0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_serialization() {
|
|
let secp = Secp256k1::new();
|
|
let config = get_test_config();
|
|
|
|
let wallet = LightWallet::new(None, &config, 0, 0).unwrap();
|
|
|
|
// First, add an incoming transaction
|
|
const AMOUNT1:u64 = 5;
|
|
|
|
let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32]));
|
|
let (nf1, txid1) = cb1.add_tx_paying(wallet.zkeys.read().unwrap()[0].extfvk.clone(), AMOUNT1);
|
|
|
|
wallet.scan_block(&cb1.as_bytes()).unwrap();
|
|
|
|
assert_eq!(wallet.txs.read().unwrap().len(), 1);
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 1);
|
|
assert_eq!(wallet.zbalance(None), AMOUNT1);
|
|
|
|
// Add a t input at the Tx
|
|
let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
|
|
const TAMOUNT1: u64 = 20;
|
|
|
|
let mut tx = FakeTransaction::new_with_txid(txid1);
|
|
tx.add_t_output(&pk, TAMOUNT1);
|
|
wallet.scan_full_tx(&tx.get_tx(), 0, 0); // Height 0
|
|
|
|
const AMOUNT2:u64 = 2;
|
|
|
|
// Add a second block, spending the first note
|
|
let addr2 = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[0u8; 32]))
|
|
.default_address().unwrap().1;
|
|
let mut cb2 = FakeCompactBlock::new(1, cb1.hash());
|
|
let txid2 = cb2.add_tx_spending((nf1, AMOUNT1), wallet.zkeys.read().unwrap()[0].extfvk.clone(), addr2, AMOUNT2);
|
|
wallet.scan_block(&cb2.as_bytes()).unwrap();
|
|
|
|
let mut tx = FakeTransaction::new_with_txid(txid2);
|
|
tx.add_t_input(txid1, 0);
|
|
wallet.scan_full_tx(&tx.get_tx(), 1, 0); // Height 1
|
|
|
|
// Now, the original note should be spent and there should be a change
|
|
assert_eq!(wallet.zbalance(None), AMOUNT1 - AMOUNT2 ); // The t addr amount is received + spent, so it cancels out
|
|
|
|
// Now, serialize the wallet and read it back again
|
|
let mut serialized_data = vec![];
|
|
wallet.write(&mut serialized_data).expect("Serialize wallet");
|
|
let wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap();
|
|
|
|
assert_eq!(wallet2.zbalance(None), AMOUNT1 - AMOUNT2);
|
|
|
|
// Test the keys were serialized correctly
|
|
{
|
|
assert_eq!(wallet.seed, wallet2.seed);
|
|
|
|
assert_eq!(wallet.zkeys.read().unwrap().len(), wallet2.zkeys.read().unwrap().len());
|
|
assert_eq!(wallet.zkeys.read().unwrap()[0].extsk, wallet2.zkeys.read().unwrap()[0].extsk);
|
|
assert_eq!(wallet.zkeys.read().unwrap()[0].extfvk, wallet2.zkeys.read().unwrap()[0].extfvk);
|
|
assert_eq!(wallet.zkeys.read().unwrap()[0].zaddress, wallet2.zkeys.read().unwrap()[0].zaddress);
|
|
|
|
assert_eq!(wallet.tkeys.read().unwrap().len(), wallet2.tkeys.read().unwrap().len());
|
|
assert_eq!(wallet.tkeys.read().unwrap()[0], wallet2.tkeys.read().unwrap()[0]);
|
|
}
|
|
|
|
// Test blocks were serialized properly
|
|
{
|
|
let blks = wallet2.blocks.read().unwrap();
|
|
|
|
assert_eq!(blks.len(), 2);
|
|
assert_eq!(blks[0].height, 0);
|
|
assert_eq!(blks[1].height, 1);
|
|
}
|
|
|
|
// Test txns were serialized properly.
|
|
{
|
|
let txs = wallet2.txs.read().unwrap();
|
|
|
|
// Old note was spent
|
|
assert_eq!(txs[&txid1].txid, txid1);
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].spent.unwrap(), txid2);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].notes[0].is_change, false);
|
|
|
|
// Old UTXO was spent
|
|
assert_eq!(txs[&txid1].utxos.len(), 1);
|
|
assert_eq!(txs[&txid1].utxos[0].address, taddr);
|
|
assert_eq!(txs[&txid1].utxos[0].txid, txid1);
|
|
assert_eq!(txs[&txid1].utxos[0].output_index, 0);
|
|
assert_eq!(txs[&txid1].utxos[0].value, TAMOUNT1);
|
|
assert_eq!(txs[&txid1].utxos[0].height, 0);
|
|
assert_eq!(txs[&txid1].utxos[0].spent, Some(txid2));
|
|
assert_eq!(txs[&txid1].utxos[0].unconfirmed_spent, None);
|
|
|
|
// new note is not spent
|
|
assert_eq!(txs[&txid2].txid, txid2);
|
|
assert_eq!(txs[&txid2].notes.len(), 1);
|
|
assert_eq!(txs[&txid2].notes[0].spent, None);
|
|
assert_eq!(txs[&txid2].notes[0].note.value, AMOUNT1 - AMOUNT2);
|
|
assert_eq!(txs[&txid2].notes[0].is_change, true);
|
|
assert_eq!(txs[&txid2].total_shielded_value_spent, AMOUNT1);
|
|
|
|
// The UTXO was spent in txid2
|
|
assert_eq!(txs[&txid2].utxos.len(), 0); // The second TxId has no UTXOs
|
|
assert_eq!(txs[&txid2].total_transparent_value_spent, TAMOUNT1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_multi_serialization() {
|
|
let config = get_test_config();
|
|
|
|
let wallet = LightWallet::new(None, &config, 0, 0).unwrap();
|
|
|
|
let taddr1 = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
let taddr2 = wallet.add_taddr();
|
|
|
|
let (zaddr1, zpk1, _) = &wallet.get_z_private_keys()[0];
|
|
let zaddr2 = wallet.add_zaddr();
|
|
|
|
let mut serialized_data = vec![];
|
|
wallet.write(&mut serialized_data).expect("Serialize wallet");
|
|
let wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap();
|
|
|
|
assert_eq!(wallet2.tkeys.read().unwrap().len(), 2);
|
|
assert_eq!(wallet2.zkeys.read().unwrap().len(), 2);
|
|
|
|
assert_eq!(taddr1, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0].tkey.unwrap()));
|
|
assert_eq!(taddr2, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[1].tkey.unwrap()));
|
|
|
|
let (w2_zaddr1, w2_zpk1, _) = &wallet.get_z_private_keys()[0];
|
|
let (w2_zaddr2, _, _) = &wallet.get_z_private_keys()[1];
|
|
assert_eq!(zaddr1, w2_zaddr1);
|
|
assert_eq!(zpk1, w2_zpk1);
|
|
assert_eq!(zaddr2, *w2_zaddr2);
|
|
|
|
}
|
|
|
|
fn get_test_config() -> LightClientConfig {
|
|
LightClientConfig {
|
|
server: "0.0.0.0:0".parse().unwrap(),
|
|
chain_name: "main".to_string(),
|
|
sapling_activation_height: 0,
|
|
consensus_branch_id: "000000".to_string(),
|
|
anchor_offset: 0,
|
|
no_cert_verification: false,
|
|
data_dir: None,
|
|
}
|
|
}
|
|
|
|
// Get a test wallet already setup with a single note
|
|
fn get_test_wallet(amount: u64) -> (LightWallet, TxId, BlockHash) {
|
|
let config = get_test_config();
|
|
|
|
let wallet = LightWallet::new(None, &config, 0, 0).unwrap();
|
|
|
|
let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32]));
|
|
let (_, txid1) = cb1.add_tx_paying(wallet.zkeys.read().unwrap()[0].extfvk.clone(), amount);
|
|
wallet.scan_block(&cb1.as_bytes()).unwrap();
|
|
|
|
// We have one note
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, amount);
|
|
assert_eq!(txs[&txid1].notes[0].spent, None);
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None);
|
|
}
|
|
|
|
assert_eq!(wallet.verified_zbalance(None), amount);
|
|
|
|
// Create a new block so that the note is now verified to be spent
|
|
let cb2 = FakeCompactBlock::new(1, cb1.hash());
|
|
wallet.scan_block(&cb2.as_bytes()).unwrap();
|
|
|
|
(wallet, txid1, cb2.hash())
|
|
}
|
|
|
|
#[test]
|
|
fn test_z_spend_to_z() {
|
|
const AMOUNT1: u64 = 50000;
|
|
let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32]));
|
|
let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(),
|
|
&fvk.default_address().unwrap().1);
|
|
|
|
const AMOUNT_SENT: u64 = 20;
|
|
|
|
let outgoing_memo = "Outgoing Memo".to_string();
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
|
|
// Make sure that the balance exists
|
|
{
|
|
assert_eq!(wallet.zbalance(None), AMOUNT1);
|
|
assert_eq!(wallet.verified_zbalance(None), AMOUNT1);
|
|
}
|
|
|
|
// Create a tx and send to address
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
// Now, the note should be unconfirmed spent
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].notes[0].is_change, false);
|
|
assert_eq!(txs[&txid1].notes[0].spent, None);
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, Some(sent_txid));
|
|
}
|
|
|
|
// It should also be in the mempool structure
|
|
{
|
|
let mem = wallet.mempool_txs.read().unwrap();
|
|
|
|
assert_eq!(mem[&sent_txid].block, 1); // block number is the current tip (the fork targets the tip, d2887d0)
|
|
assert! (mem[&sent_txid].datetime > 0);
|
|
assert_eq!(mem[&sent_txid].txid, sent_txid);
|
|
assert_eq!(mem[&sent_txid].outgoing_metadata.len(), 1);
|
|
assert_eq!(mem[&sent_txid].outgoing_metadata[0].address, ext_address);
|
|
assert_eq!(mem[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT);
|
|
assert_eq!(mem[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo);
|
|
}
|
|
|
|
{
|
|
// The wallet should deduct this from the verified balance. The zbalance excludes it too
|
|
// (since the fork's mempool integration, eff5dd7, pending spends are not in zbalance)
|
|
assert_eq!(wallet.zbalance(None), 0);
|
|
assert_eq!(wallet.verified_zbalance(None), 0);
|
|
}
|
|
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
|
|
// Now this new Spent tx should be in, so the note should be marked confirmed spent
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid));
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None);
|
|
|
|
// The sent tx should generate change
|
|
assert_eq!(txs[&sent_txid].notes.len(), 1);
|
|
assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee);
|
|
assert_eq!(wallet.zbalance(None), AMOUNT1 - AMOUNT_SENT - fee);
|
|
assert_eq!(txs[&sent_txid].notes[0].is_change, true);
|
|
assert_eq!(txs[&sent_txid].notes[0].spent, None);
|
|
assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None);
|
|
}
|
|
|
|
{
|
|
// And the mempool tx should disappear
|
|
let mem = wallet.mempool_txs.read().unwrap();
|
|
assert!(mem.get(&sent_txid).is_none());
|
|
}
|
|
|
|
// Now, full scan the Tx, which should populate the Outgoing Meta data
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
// Check Outgoing Metadata
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1);
|
|
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1);
|
|
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, ext_address);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_self_txns_ttoz_withmemo() {
|
|
let mut rng = OsRng;
|
|
let secp = Secp256k1::new();
|
|
|
|
let (wallet, _txid1, block_hash) = get_test_wallet(0);
|
|
|
|
let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
|
|
const TAMOUNT1: u64 = 50000;
|
|
|
|
let mut tx = FakeTransaction::new(&mut rng);
|
|
tx.add_t_output(&pk, TAMOUNT1);
|
|
let txid1 = tx.get_tx().txid();
|
|
|
|
wallet.scan_full_tx(&tx.get_tx(), 1, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// Now make sure the t addr was recieved
|
|
assert_eq!(txs[&txid1].utxos.len(), 1);
|
|
assert_eq!(txs[&txid1].utxos[0].address, taddr);
|
|
assert_eq!(txs[&txid1].utxos[0].value, TAMOUNT1);
|
|
}
|
|
|
|
// Create a new Tx, spending this taddr
|
|
const AMOUNT_SENT: u64 = 20;
|
|
|
|
let outgoing_memo = "Outgoing Memo".to_string();
|
|
let zaddr = wallet.add_zaddr();
|
|
|
|
let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap();
|
|
let (ss, so) =get_sapling_params().unwrap();
|
|
|
|
// Create a tx and send to address. This should consume both the UTXO and the note
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&zaddr, AMOUNT_SENT, Some(outgoing_memo.clone()))], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
|
|
// Scan the compact block and the full Tx
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// Includes Outgoing meta data, since this is a wallet -> wallet tx with a memo
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_self_txns_ttoz_nomemo() {
|
|
let mut rng = OsRng;
|
|
let secp = Secp256k1::new();
|
|
|
|
let (wallet, _txid1, block_hash) = get_test_wallet(0);
|
|
|
|
let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
|
|
const TAMOUNT1: u64 = 50000;
|
|
|
|
let mut tx = FakeTransaction::new(&mut rng);
|
|
tx.add_t_output(&pk, TAMOUNT1);
|
|
let txid1 = tx.get_tx().txid();
|
|
|
|
wallet.scan_full_tx(&tx.get_tx(), 1, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// Now make sure the t addr was recieved
|
|
assert_eq!(txs[&txid1].utxos.len(), 1);
|
|
assert_eq!(txs[&txid1].utxos[0].address, taddr);
|
|
assert_eq!(txs[&txid1].utxos[0].value, TAMOUNT1);
|
|
}
|
|
|
|
// Create a new Tx, spending this taddr
|
|
const AMOUNT_SENT: u64 = 20;
|
|
|
|
let zaddr = wallet.add_zaddr();
|
|
|
|
let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap();
|
|
let (ss, so) =get_sapling_params().unwrap();
|
|
|
|
// Create a tx and send to address. This should consume both the UTXO and the note
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&zaddr, AMOUNT_SENT, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
|
|
// Scan the compact block and the full Tx
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// No Outgoing meta data, since this is a wallet -> wallet tx without a memo
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 0);
|
|
}
|
|
}
|
|
|
|
/// R3 (regression test). The mempool monitor (scan_full_mempool_tx) records our send in `txs` at the height
|
|
/// the server reports. The block that mines it must become its height: a reorg of that block removes the
|
|
/// send, un-spends its note and drops its change. Before the fix the entry kept the monitor's height.
|
|
#[test]
|
|
fn test_r3_mempool_seen_send_takes_mined_height() {
|
|
const AMOUNT1: u64 = 50000;
|
|
let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1); // note at block 0, tip 1
|
|
let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32]));
|
|
let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(), &fvk.default_address().unwrap().1);
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&ext_address, 20, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
wallet.scan_full_mempool_tx(&sent_tx, 1, 0, true); // decrypts the change: entry at the server height 1
|
|
assert_eq!(wallet.txs.read().unwrap().get(&sent_txid).map(|t| t.block), Some(1), "setup: monitor entry");
|
|
|
|
let cb2 = FakeCompactBlock::new(2, block_hash); // not in block 2
|
|
wallet.scan_block(&cb2.as_bytes()).unwrap();
|
|
let mut cb3 = FakeCompactBlock::new(3, cb2.hash()); // mined in block 3
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
assert_eq!(wallet.txs.read().unwrap()[&sent_txid].block, 3,
|
|
"R3 (regression): mined at 3, but the WalletTx keeps the mempool monitor's height 1");
|
|
|
|
assert_eq!(wallet.invalidate_block(3), 1);
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert!(txs.get(&sent_txid).is_none(), "R3 (regression): a reorg of its mining block kept the send");
|
|
assert_eq!(txs[&txid1].notes[0].spent, None, "R3 (regression): phantom spend");
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None);
|
|
assert!(!txs[&txid1].notes[0].witnesses.is_empty());
|
|
}
|
|
assert_eq!(wallet.zbalance(None), AMOUNT1); // guard dropped first: zbalance takes txs.read itself
|
|
}
|
|
|
|
#[test]
|
|
fn test_self_txns_ztoz() {
|
|
const AMOUNT1: u64 = 50000;
|
|
let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
let zaddr2 = wallet.add_zaddr(); // This is acually address #6, since there are 5 initial addresses in the wallet
|
|
|
|
const AMOUNT_SENT: u64 = 20;
|
|
|
|
let outgoing_memo = "Outgoing Memo".to_string();
|
|
|
|
let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap();
|
|
let (ss, so) =get_sapling_params().unwrap();
|
|
|
|
// Create a tx and send to address
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&zaddr2, AMOUNT_SENT, Some(outgoing_memo.clone()))], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// Includes Outgoing meta data, since this is a wallet -> wallet tx with a memo
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo);
|
|
}
|
|
|
|
// Another self tx, this time without a memo
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&zaddr2, AMOUNT_SENT, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
let mut cb4 = FakeCompactBlock::new(3, cb3.hash());
|
|
cb4.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb4.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 3, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// No Outgoing meta data, since this is a wallet -> wallet tx without a memo
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 0);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
fn test_multi_z() {
|
|
const AMOUNT1: u64 = 50000;
|
|
let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
let zaddr2 = wallet.add_zaddr(); // This is acually address #6, since there are 5 initial addresses in the wallet
|
|
|
|
const AMOUNT_SENT: u64 = 20;
|
|
|
|
let outgoing_memo = "Outgoing Memo".to_string();
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) =get_sapling_params().unwrap();
|
|
|
|
// Create a tx and send to address
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&zaddr2, AMOUNT_SENT, Some(outgoing_memo.clone()))], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
// Because the builder will randomize notes outputted, we need to find
|
|
// which note number is the change and which is the output note (Because this tx
|
|
// had both outputs in the same Tx)
|
|
let (change_note_number, ext_note_number) = {
|
|
let txs = wallet.txs.read().unwrap();
|
|
if txs[&sent_txid].notes[0].is_change { (0,1) } else { (1,0) }
|
|
};
|
|
|
|
// Now this new Spent tx should be in, so the note should be marked confirmed spent
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid));
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None);
|
|
|
|
// The sent tx should generate change + the new incoming note
|
|
assert_eq!(txs[&sent_txid].notes.len(), 2);
|
|
|
|
assert_eq!(txs[&sent_txid].notes[change_note_number].note.value, AMOUNT1 - AMOUNT_SENT - fee);
|
|
assert_eq!(txs[&sent_txid].notes[change_note_number].account, 0);
|
|
assert_eq!(txs[&sent_txid].notes[change_note_number].is_change, true);
|
|
assert_eq!(txs[&sent_txid].notes[change_note_number].spent, None);
|
|
assert_eq!(txs[&sent_txid].notes[change_note_number].unconfirmed_spent, None);
|
|
assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[change_note_number].memo), None);
|
|
|
|
assert_eq!(txs[&sent_txid].notes[ext_note_number].note.value, AMOUNT_SENT);
|
|
assert_eq!(txs[&sent_txid].notes[ext_note_number].account, 6); // The new addr is added after the change addresses
|
|
assert_eq!(txs[&sent_txid].notes[ext_note_number].is_change, false);
|
|
assert_eq!(txs[&sent_txid].notes[ext_note_number].spent, None);
|
|
assert_eq!(txs[&sent_txid].notes[ext_note_number].unconfirmed_spent, None);
|
|
assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[ext_note_number].memo), Some(outgoing_memo.clone()));
|
|
|
|
assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1);
|
|
|
|
// Includes Outgoing meta data, since this is a wallet -> wallet tx with a memo
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo);
|
|
}
|
|
|
|
// Now spend the money, which should pick notes from both addresses
|
|
let amount_all:u64 = (AMOUNT1 - AMOUNT_SENT - fee) + (AMOUNT_SENT) - fee;
|
|
let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap());
|
|
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&taddr, amount_all, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_ext_txid = sent_tx.txid();
|
|
|
|
let mut cb4 = FakeCompactBlock::new(3, cb3.hash());
|
|
cb4.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb4.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 3, 0);
|
|
|
|
{
|
|
// Both notes should be spent now.
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
assert_eq!(txs[&sent_txid].notes[change_note_number].is_change, true);
|
|
assert_eq!(txs[&sent_txid].notes[change_note_number].spent, Some(sent_ext_txid));
|
|
assert_eq!(txs[&sent_txid].notes[change_note_number].unconfirmed_spent, None);
|
|
|
|
assert_eq!(txs[&sent_txid].notes[ext_note_number].is_change, false);
|
|
assert_eq!(txs[&sent_txid].notes[ext_note_number].spent, Some(sent_ext_txid));
|
|
assert_eq!(txs[&sent_txid].notes[ext_note_number].unconfirmed_spent, None);
|
|
|
|
// Check outgoing metadata for the external sent tx
|
|
assert_eq!(txs[&sent_ext_txid].notes.len(), 0); // No change was generated
|
|
assert_eq!(txs[&sent_ext_txid].outgoing_metadata.len(), 1);
|
|
assert_eq!(txs[&sent_ext_txid].outgoing_metadata[0].address, taddr);
|
|
assert_eq!(txs[&sent_ext_txid].outgoing_metadata[0].value, amount_all);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_z_spend_to_taddr() {
|
|
const AMOUNT1: u64 = 50000;
|
|
let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
|
|
let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap());
|
|
const AMOUNT_SENT: u64 = 30;
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&taddr, AMOUNT_SENT, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
// Now, the note should be unconfirmed spent
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].notes[0].is_change, false);
|
|
assert_eq!(txs[&txid1].notes[0].spent, None);
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, Some(sent_txid));
|
|
}
|
|
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
|
|
// Now this new Spent tx should be in, so the note should be marked confirmed spent
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid));
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None);
|
|
|
|
// The sent tx should generate change
|
|
assert_eq!(txs[&sent_txid].notes.len(), 1);
|
|
assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee);
|
|
assert_eq!(txs[&sent_txid].notes[0].is_change, true);
|
|
assert_eq!(txs[&sent_txid].notes[0].spent, None);
|
|
assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None);
|
|
}
|
|
|
|
// Now, full scan the Tx, which should populate the Outgoing Meta data
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
// Check Outgoing Metadata for t address
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, taddr);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT);
|
|
assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1);
|
|
}
|
|
|
|
// Create a new Tx, but this time with a memo.
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&taddr, AMOUNT_SENT, Some("T address memo".to_string()))], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid2 = sent_tx.txid();
|
|
|
|
// There should be a mempool Tx, but the memo should be dropped, because it was sent to a
|
|
// t address
|
|
{
|
|
let txs = wallet.mempool_txs.read().unwrap();
|
|
|
|
assert_eq!(txs[&sent_txid2].outgoing_metadata.len(), 1);
|
|
assert_eq!(txs[&sent_txid2].outgoing_metadata[0].address, taddr);
|
|
assert_eq!(txs[&sent_txid2].outgoing_metadata[0].value, AMOUNT_SENT);
|
|
assert_eq!(LightWallet::memo_str(&Some(txs[&sent_txid2].outgoing_metadata[0].memo.clone())), None);
|
|
}
|
|
|
|
// Now add the block
|
|
let mut cb4 = FakeCompactBlock::new(3, cb3.hash());
|
|
cb4.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb4.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 3, 0);
|
|
|
|
// Check Outgoing Metadata for t address, but once again there should be no memo
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&sent_txid2].outgoing_metadata.len(), 1);
|
|
assert_eq!(txs[&sent_txid2].outgoing_metadata[0].address, taddr);
|
|
assert_eq!(txs[&sent_txid2].outgoing_metadata[0].value, AMOUNT_SENT);
|
|
assert_eq!(LightWallet::memo_str(&Some(txs[&sent_txid2].outgoing_metadata[0].memo.clone())), None);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_t_spend_to_z() {
|
|
let mut rng = OsRng;
|
|
let secp = Secp256k1::new();
|
|
|
|
const AMOUNT_Z: u64 = 50000;
|
|
const AMOUNT_T: u64 = 40000;
|
|
let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT_Z);
|
|
|
|
let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
|
|
let mut tx = FakeTransaction::new(&mut rng);
|
|
tx.add_t_output(&pk, AMOUNT_T);
|
|
let txid_t = tx.get_tx().txid();
|
|
|
|
wallet.scan_full_tx(&tx.get_tx(), 1, 0); // Pretend it is at height 1
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// Now make sure the t addr was recieved
|
|
assert_eq!(txs[&txid_t].utxos.len(), 1);
|
|
assert_eq!(txs[&txid_t].utxos[0].address, taddr);
|
|
assert_eq!(txs[&txid_t].utxos[0].spent, None);
|
|
assert_eq!(txs[&txid_t].utxos[0].unconfirmed_spent, None);
|
|
|
|
assert_eq!(wallet.tbalance(None), AMOUNT_T);
|
|
}
|
|
|
|
|
|
let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32]));
|
|
let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(),
|
|
&fvk.default_address().unwrap().1);
|
|
const AMOUNT_SENT: u64 = 20;
|
|
|
|
let outgoing_memo = "Outgoing Memo".to_string();
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) =get_sapling_params().unwrap();
|
|
|
|
// Create a tx and send to address. This should consume both the UTXO and the note
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
// Verify the sent_tx for sanity
|
|
{
|
|
// The tx has 1 note spent, 1 utxo spent, and (1 note out, 1 note change)
|
|
assert_eq!(sent_tx.shielded_spends.len(), 1);
|
|
assert_eq!(sent_tx.vin.len(), 1);
|
|
assert_eq!(sent_tx.shielded_outputs.len(), 2);
|
|
}
|
|
|
|
// Now, the note and utxo should be unconfirmed spent
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// UTXO
|
|
assert_eq!(txs[&txid_t].utxos.len(), 1);
|
|
assert_eq!(txs[&txid_t].utxos[0].address, taddr);
|
|
assert_eq!(txs[&txid_t].utxos[0].spent, None);
|
|
assert_eq!(txs[&txid_t].utxos[0].unconfirmed_spent, Some(sent_txid));
|
|
|
|
// Note
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT_Z);
|
|
assert_eq!(txs[&txid1].notes[0].spent, None);
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, Some(sent_txid));
|
|
}
|
|
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
|
|
// Scan the compact block and the full Tx
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
// Now this new Spent tx should be in, so the note should be marked confirmed spent
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT_Z);
|
|
assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid));
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None);
|
|
|
|
// The UTXO should also be spent
|
|
assert_eq!(txs[&txid_t].utxos[0].address, taddr);
|
|
assert_eq!(txs[&txid_t].utxos[0].spent, Some(sent_txid));
|
|
assert_eq!(txs[&txid_t].utxos[0].unconfirmed_spent, None);
|
|
|
|
// The sent tx should generate change
|
|
assert_eq!(txs[&sent_txid].notes.len(), 1);
|
|
assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT_Z + AMOUNT_T - AMOUNT_SENT - fee);
|
|
assert_eq!(txs[&sent_txid].notes[0].is_change, true);
|
|
assert_eq!(txs[&sent_txid].notes[0].spent, None);
|
|
assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_z_incoming_memo() {
|
|
const AMOUNT1: u64 = 50000;
|
|
let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
let my_address = encode_payment_address(wallet.config.hrp_sapling_address(),
|
|
&wallet.zkeys.read().unwrap()[0].extfvk.default_address().unwrap().1);
|
|
|
|
let memo = "Incoming Memo".to_string();
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
|
|
// Create a tx and send to address
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&my_address, AMOUNT1 - fee, Some(memo.clone()))], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
// Add it to a block
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
|
|
// And scan the Full Tx to get the memo
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
assert_eq!(txs[&sent_txid].notes.len(), 1);
|
|
|
|
assert_eq!(txs[&sent_txid].notes[0].extfvk, wallet.zkeys.read().unwrap()[0].extfvk);
|
|
assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - fee);
|
|
assert_eq!(LightWallet::note_address(wallet.config.hrp_sapling_address(), &txs[&sent_txid].notes[0]), Some(my_address));
|
|
assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[0].memo), Some(memo));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_new_zt_hd_after_incoming() {
|
|
// When an address recieves funds, a new, unused address should automatically get added
|
|
const AMOUNT1: u64 = 50000;
|
|
let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
// Get the last address
|
|
let my_address = encode_payment_address(wallet.config.hrp_sapling_address(),
|
|
&wallet.zkeys.read().unwrap().last().unwrap().extfvk.default_address().unwrap().1);
|
|
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
|
|
assert_eq!(wallet.zkeys.read().unwrap().len(), 6); // Starts with 1+5 addresses
|
|
|
|
// Create a tx and send to the last address
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&my_address, AMOUNT1 - fee, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
|
|
// Add it to a block
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
|
|
// NOw, 5 new addresses should be created
|
|
assert_eq!(wallet.zkeys.read().unwrap().len(), 6+5);
|
|
|
|
let mut rng = OsRng;
|
|
let secp = Secp256k1::new();
|
|
// Send a fake transaction to the last taddr
|
|
let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap().last().unwrap().tkey.unwrap());
|
|
|
|
// Start with 1 taddr
|
|
assert_eq!(wallet.tkeys.read().unwrap().len(), 1);
|
|
|
|
// Send a Tx to the last address
|
|
let mut tx = FakeTransaction::new(&mut rng);
|
|
tx.add_t_output(&pk, AMOUNT1);
|
|
wallet.scan_full_tx(&tx.get_tx(), 3, 0);
|
|
|
|
// Now, 5 new addresses should be created.
|
|
assert_eq!(wallet.tkeys.read().unwrap().len(), 1+5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_z_to_t_withinwallet() {
|
|
const AMOUNT: u64 = 500000;
|
|
const AMOUNT_SENT: u64 = 20000;
|
|
let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT);
|
|
|
|
let taddr = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
|
|
// Create a tx and send to address
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&taddr, AMOUNT_SENT, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
// Add it to a block
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
|
|
// And scan the Full Tx to get the memo
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// We have the original note
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT);
|
|
|
|
// We have the spent tx
|
|
assert_eq!(txs[&sent_txid].notes.len(), 1);
|
|
assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT - AMOUNT_SENT - fee);
|
|
assert_eq!(txs[&sent_txid].notes[0].is_change, true);
|
|
assert_eq!(txs[&sent_txid].notes[0].spent, None);
|
|
assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None);
|
|
|
|
// Since we sent the Tx to ourself, there should be no outgoing
|
|
// metadata
|
|
assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 0);
|
|
|
|
|
|
// We have the taddr utxo in the same Tx
|
|
assert_eq!(txs[&sent_txid].utxos.len(), 1);
|
|
assert_eq!(txs[&sent_txid].utxos[0].address, taddr);
|
|
assert_eq!(txs[&sent_txid].utxos[0].value, AMOUNT_SENT);
|
|
assert_eq!(txs[&sent_txid].utxos[0].spent, None);
|
|
assert_eq!(txs[&sent_txid].utxos[0].unconfirmed_spent, None);
|
|
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_multi_t() {
|
|
const AMOUNT: u64 = 5000000;
|
|
const AMOUNT_SENT1: u64 = 20000;
|
|
const AMOUNT_SENT2: u64 = 10000;
|
|
|
|
let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT);
|
|
|
|
// Add a new taddr
|
|
let taddr2 = wallet.add_taddr();
|
|
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
|
|
// Create a Tx and send to the second t address
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&taddr2, AMOUNT_SENT1, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid1 = sent_tx.txid();
|
|
|
|
// Add it to a block
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
// Check that the send to the second taddr worked
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// We have the original note
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT);
|
|
|
|
// We have the spent tx
|
|
assert_eq!(txs[&sent_txid1].notes.len(), 1);
|
|
assert_eq!(txs[&sent_txid1].notes[0].note.value, AMOUNT - AMOUNT_SENT1 - fee);
|
|
assert_eq!(txs[&sent_txid1].notes[0].is_change, true);
|
|
assert_eq!(txs[&sent_txid1].notes[0].spent, None);
|
|
assert_eq!(txs[&sent_txid1].notes[0].unconfirmed_spent, None);
|
|
|
|
// Since we sent the Tx to ourself, there should be no outgoing
|
|
// metadata
|
|
assert_eq!(txs[&sent_txid1].total_shielded_value_spent, AMOUNT);
|
|
assert_eq!(txs[&sent_txid1].outgoing_metadata.len(), 0);
|
|
|
|
|
|
// We have the taddr utxo in the same Tx
|
|
assert_eq!(txs[&sent_txid1].utxos.len(), 1);
|
|
assert_eq!(txs[&sent_txid1].utxos[0].address, taddr2);
|
|
assert_eq!(txs[&sent_txid1].utxos[0].value, AMOUNT_SENT1);
|
|
assert_eq!(txs[&sent_txid1].utxos[0].spent, None);
|
|
assert_eq!(txs[&sent_txid1].utxos[0].unconfirmed_spent, None);
|
|
}
|
|
|
|
// Send some money to the 3rd t addr
|
|
let taddr3 = wallet.add_taddr();
|
|
|
|
// Create a Tx and send to the second t address
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&taddr3, AMOUNT_SENT2, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid2 = sent_tx.txid();
|
|
|
|
// Add it to a block
|
|
let mut cb4 = FakeCompactBlock::new(3, cb3.hash());
|
|
cb4.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb4.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 3, 0);
|
|
|
|
// Quickly check we have it
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// We have the taddr utxo in the same Tx
|
|
assert_eq!(txs[&sent_txid2].utxos.len(), 1);
|
|
assert_eq!(txs[&sent_txid2].utxos[0].address, taddr3);
|
|
assert_eq!(txs[&sent_txid2].utxos[0].value, AMOUNT_SENT2);
|
|
|
|
// Old UTXO was spent here
|
|
assert_eq!(txs[&sent_txid1].utxos.len(), 1);
|
|
assert_eq!(txs[&sent_txid1].utxos[0].value, AMOUNT_SENT1);
|
|
assert_eq!(txs[&sent_txid1].utxos[0].address, taddr2);
|
|
assert_eq!(txs[&sent_txid1].utxos[0].spent, Some(sent_txid2));
|
|
assert_eq!(txs[&sent_txid1].utxos[0].unconfirmed_spent, None);
|
|
}
|
|
|
|
// Now, spend to an external z address, which will select all the utxos
|
|
let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32]));
|
|
let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(),
|
|
&fvk.default_address().unwrap().1);
|
|
|
|
const AMOUNT_SENT_EXT: u64 = 45;
|
|
let outgoing_memo = "Outgoing Memo".to_string();
|
|
|
|
// Create a tx and send to address
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&ext_address, AMOUNT_SENT_EXT, Some(outgoing_memo.clone()))], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid3 = sent_tx.txid();
|
|
|
|
let mut cb5 = FakeCompactBlock::new(4, cb4.hash());
|
|
cb5.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb5.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 4, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&sent_txid3].outgoing_metadata.len(), 1);
|
|
|
|
assert_eq!(txs[&sent_txid3].outgoing_metadata[0].address, ext_address);
|
|
assert_eq!(txs[&sent_txid3].outgoing_metadata[0].value, AMOUNT_SENT_EXT);
|
|
assert_eq!(txs[&sent_txid3].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo);
|
|
|
|
// Test to see that the UTXOs were spent.
|
|
|
|
// UTXO2
|
|
assert_eq!(txs[&sent_txid2].utxos[0].value, AMOUNT_SENT2);
|
|
assert_eq!(txs[&sent_txid2].utxos[0].address, taddr3);
|
|
assert_eq!(txs[&sent_txid2].utxos[0].spent, Some(sent_txid3));
|
|
assert_eq!(txs[&sent_txid2].utxos[0].unconfirmed_spent, None);
|
|
}
|
|
|
|
}
|
|
|
|
#[test]
|
|
fn test_multi_spends() {
|
|
const AMOUNT1: u64 = 50000;
|
|
let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
let zaddr2 = wallet.add_zaddr(); // Address number 6
|
|
const ZAMOUNT2:u64 = 30;
|
|
let outgoing_memo2 = "Outgoing Memo2".to_string();
|
|
|
|
let zaddr3 = wallet.add_zaddr(); // Address number 7
|
|
const ZAMOUNT3:u64 = 40;
|
|
let outgoing_memo3 = "Outgoing Memo3".to_string();
|
|
|
|
let taddr2 = wallet.add_taddr();
|
|
const TAMOUNT2:u64 = 50;
|
|
let taddr3 = wallet.add_taddr();
|
|
const TAMOUNT3:u64 = 60;
|
|
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
|
|
let tos = vec![ (zaddr2.as_str(), ZAMOUNT2, Some(outgoing_memo2.clone())),
|
|
(zaddr3.as_str(), ZAMOUNT3, Some(outgoing_memo3.clone())),
|
|
(taddr2.as_str(), TAMOUNT2, None),
|
|
(taddr3.as_str(), TAMOUNT3, None) ];
|
|
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false, tos, &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
// Make sure all the outputs are there!
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// The single note was spent
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid));
|
|
|
|
// The outputs are all sent to the wallet, so they should
|
|
// correspond to notes & utxos.
|
|
// 2 notes + 1 change
|
|
assert_eq!(txs[&sent_txid].notes.len(), 3);
|
|
|
|
// Find the change note
|
|
let change_note = txs[&sent_txid].notes.iter().find(|n| n.is_change).unwrap();
|
|
assert_eq!(change_note.note.value, AMOUNT1 - (ZAMOUNT2+ZAMOUNT3+TAMOUNT2+TAMOUNT3+fee));
|
|
assert_eq!(change_note.spent, None);
|
|
assert_eq!(change_note.unconfirmed_spent, None);
|
|
|
|
// Find zaddr2
|
|
let zaddr2_note = txs[&sent_txid].notes.iter().find(|n| n.note.value == ZAMOUNT2).unwrap();
|
|
assert_eq!(zaddr2_note.account, 6);
|
|
assert_eq!(zaddr2_note.is_change, false);
|
|
assert_eq!(zaddr2_note.spent, None);
|
|
assert_eq!(zaddr2_note.unconfirmed_spent, None);
|
|
assert_eq!(LightWallet::memo_str(&zaddr2_note.memo), Some(outgoing_memo2));
|
|
|
|
// Find zaddr3
|
|
let zaddr3_note = txs[&sent_txid].notes.iter().find(|n| n.note.value == ZAMOUNT3).unwrap();
|
|
assert_eq!(zaddr3_note.account, 7);
|
|
assert_eq!(zaddr3_note.is_change, false);
|
|
assert_eq!(zaddr3_note.spent, None);
|
|
assert_eq!(zaddr3_note.unconfirmed_spent, None);
|
|
assert_eq!(LightWallet::memo_str(&zaddr3_note.memo), Some(outgoing_memo3));
|
|
|
|
// Find taddr2
|
|
let utxo2 = txs[&sent_txid].utxos.iter().find(|u| u.value == TAMOUNT2).unwrap();
|
|
assert_eq!(utxo2.address, taddr2);
|
|
assert_eq!(utxo2.txid, sent_txid);
|
|
assert_eq!(utxo2.spent, None);
|
|
assert_eq!(utxo2.unconfirmed_spent, None);
|
|
|
|
// Find taddr3
|
|
let utxo3 = txs[&sent_txid].utxos.iter().find(|u| u.value == TAMOUNT3).unwrap();
|
|
assert_eq!(utxo3.address, taddr3);
|
|
assert_eq!(utxo3.txid, sent_txid);
|
|
assert_eq!(utxo3.spent, None);
|
|
assert_eq!(utxo3.unconfirmed_spent, None);
|
|
}
|
|
|
|
// Now send an outgoing tx to one ext taddr and one ext zaddr
|
|
let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32]));
|
|
let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(),
|
|
&fvk.default_address().unwrap().1);
|
|
let ext_memo = "External memo".to_string();
|
|
let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap());
|
|
|
|
const EXT_ZADDR_AMOUNT: u64 = 3000;
|
|
let ext_taddr_amount = AMOUNT1 - fee - EXT_ZADDR_AMOUNT - fee; // Spend everything
|
|
println!("taddr amount {}", ext_taddr_amount);
|
|
|
|
let tos = vec![ (ext_address.as_str(), EXT_ZADDR_AMOUNT, Some(ext_memo.clone())),
|
|
(ext_taddr.as_str(), ext_taddr_amount, None)];
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false, tos, &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid2 = sent_tx.txid();
|
|
|
|
let mut cb4 = FakeCompactBlock::new(3, cb3.hash());
|
|
cb4.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb4.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 3, 0);
|
|
|
|
// Make sure all the outputs are there!
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// All notes were spent
|
|
assert_eq!(txs[&sent_txid].notes[0].spent, Some(sent_txid2));
|
|
assert_eq!(txs[&sent_txid].notes[1].spent, Some(sent_txid2));
|
|
assert_eq!(txs[&sent_txid].notes[2].spent, Some(sent_txid2));
|
|
|
|
// All utxos were spent
|
|
assert_eq!(txs[&sent_txid].utxos[0].spent, Some(sent_txid2));
|
|
assert_eq!(txs[&sent_txid].utxos[1].spent, Some(sent_txid2));
|
|
|
|
// The new tx has no change
|
|
assert_eq!(txs[&sent_txid2].notes.len(), 0);
|
|
assert_eq!(txs[&sent_txid2].utxos.len(), 0);
|
|
|
|
// Test the outgoing metadata
|
|
// Find the znote
|
|
let zoutgoing = txs[&sent_txid2].outgoing_metadata.iter().find(|o| o.address == ext_address).unwrap();
|
|
assert_eq!(zoutgoing.value, EXT_ZADDR_AMOUNT);
|
|
assert_eq!(LightWallet::memo_str(&Some(zoutgoing.memo.clone())), Some(ext_memo));
|
|
|
|
// Find the taddr
|
|
let toutgoing = txs[&sent_txid2].outgoing_metadata.iter().find(|o| o.address == ext_taddr).unwrap();
|
|
assert_eq!(toutgoing.value, ext_taddr_amount);
|
|
assert_eq!(LightWallet::memo_str(&Some(toutgoing.memo.clone())), None);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_bad_send() {
|
|
// Test all the ways in which a send should fail
|
|
const AMOUNT1: u64 = 50000;
|
|
let _fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let (wallet, _txid1, _block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap());
|
|
|
|
// Bad address
|
|
let raw_tx = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&"badaddress", 10, None)], &default_fee(), |_| Ok(' '.to_string()));
|
|
assert!(raw_tx.err().unwrap().contains("Invalid recipient address"));
|
|
|
|
// Insufficient funds
|
|
let raw_tx = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&ext_taddr, AMOUNT1 + 10, None)], &default_fee(), |_| Ok(' '.to_string()));
|
|
assert!(raw_tx.err().unwrap().contains("Insufficient verified funds"));
|
|
|
|
// No addresses
|
|
let raw_tx = wallet.send_to_address(branch_id, &ss, &so, false, vec![], &default_fee(), |_| Ok(' '.to_string()));
|
|
assert!(raw_tx.err().unwrap().contains("at least one"));
|
|
|
|
}
|
|
|
|
#[test]
|
|
fn test_duplicate_outputs() {
|
|
// Test all the ways in which a send should fail
|
|
const AMOUNT1: u64 = 50000;
|
|
let _fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let (wallet, _txid1, _block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
let branch_id = u32::from_str_radix("2bb40e60", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap());
|
|
|
|
// Duplicated addresses with memos are fine too
|
|
let raw_tx = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&ext_taddr, 100, Some("First memo".to_string())),
|
|
(&ext_taddr, 0, Some("Second memo".to_string())),
|
|
(&ext_taddr, 0, Some("Third memo".to_string()))], &default_fee(), |_| Ok(' '.to_string()));
|
|
assert!(raw_tx.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn test_bad_params() {
|
|
let (wallet, _, _) = get_test_wallet(100000);
|
|
let ext_taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap());
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
// Bad params
|
|
let _ = wallet.send_to_address(branch_id, &[], &[], false,
|
|
vec![(&ext_taddr, 10, None)], &default_fee(), |_| Ok(' '.to_string()));
|
|
}
|
|
|
|
/// Test helper to add blocks
|
|
fn add_blocks(wallet: &LightWallet, start: i32, num: i32, mut prev_hash: BlockHash) -> Result<BlockHash, i32>{
|
|
// Add it to a block
|
|
let mut new_blk = FakeCompactBlock::new(start, prev_hash);
|
|
for i in 0..num {
|
|
new_blk = FakeCompactBlock::new(start+i, prev_hash);
|
|
prev_hash = new_blk.hash();
|
|
match wallet.scan_block(&new_blk.as_bytes()) {
|
|
Ok(_) => {}, // continue
|
|
Err(e) => return Err(e)
|
|
};
|
|
}
|
|
|
|
Ok(new_blk.hash())
|
|
}
|
|
|
|
#[test]
|
|
fn test_z_mempool_expiry() {
|
|
const AMOUNT1: u64 = 50000;
|
|
let (wallet, _, block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32]));
|
|
let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(),
|
|
&fvk.default_address().unwrap().1);
|
|
|
|
const AMOUNT_SENT: u64 = 20;
|
|
|
|
let outgoing_memo = "Outgoing Memo".to_string();
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
|
|
// Create a tx and send to address
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
// It should also be in the mempool structure
|
|
{
|
|
let mem = wallet.mempool_txs.read().unwrap();
|
|
|
|
assert_eq!(mem[&sent_txid].block, 1); // block number is the current tip (the fork targets the tip, d2887d0)
|
|
assert! (mem[&sent_txid].datetime > 0);
|
|
assert_eq!(mem[&sent_txid].txid, sent_txid);
|
|
assert_eq!(mem[&sent_txid].outgoing_metadata.len(), 1);
|
|
assert_eq!(mem[&sent_txid].outgoing_metadata[0].address, ext_address);
|
|
assert_eq!(mem[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT);
|
|
assert_eq!(mem[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo);
|
|
}
|
|
|
|
// Don't mine the Tx, but just add several blocks
|
|
add_blocks(&wallet, 2, 21, block_hash).unwrap();
|
|
|
|
// After 21 blocks, it should disappear (expiry is 20 blocks) since it was not mined
|
|
{
|
|
let mem = wallet.mempool_txs.read().unwrap();
|
|
|
|
assert!(mem.get(&sent_txid).is_none());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_block_limit() {
|
|
const AMOUNT: u64 = 500000;
|
|
let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT);
|
|
|
|
let prev_hash = add_blocks(&wallet, 2, 1, block_hash).unwrap();
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 3);
|
|
|
|
let prev_hash = add_blocks(&wallet, 3, 47, prev_hash).unwrap();
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 50);
|
|
|
|
let prev_hash = add_blocks(&wallet, 50, 51, prev_hash).unwrap();
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 101);
|
|
|
|
// Subsequent blocks should start to trim
|
|
let prev_hash = add_blocks(&wallet, 101, 1, prev_hash).unwrap();
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 101);
|
|
|
|
// Add lots
|
|
let _ = add_blocks(&wallet, 102, 10, prev_hash).unwrap();
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 101);
|
|
|
|
// Now clear the blocks
|
|
wallet.clear_blocks();
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 0);
|
|
|
|
let prev_hash = add_blocks(&wallet, 0, 1, BlockHash([0;32])).unwrap();
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 1);
|
|
|
|
let _ = add_blocks(&wallet, 1, 10, prev_hash).unwrap();
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 11);
|
|
}
|
|
|
|
#[test]
|
|
fn test_rollback() {
|
|
const AMOUNT: u64 = 500000;
|
|
|
|
let (wallet, txid1, block_hash) = get_test_wallet(AMOUNT);
|
|
|
|
add_blocks(&wallet, 2, 5, block_hash).unwrap();
|
|
|
|
// Make sure the note exists with the witnesses
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid1].notes[0].witnesses.len(), 7);
|
|
}
|
|
|
|
// Invalidate 2 blocks
|
|
assert_eq!(wallet.last_scanned_height(), 6);
|
|
assert_eq!(wallet.invalidate_block(5), 2);
|
|
|
|
// THe witnesses should be rolledback
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid1].notes[0].witnesses.len(), 5);
|
|
}
|
|
|
|
let blk3_hash;
|
|
let blk4_hash;
|
|
{
|
|
let blks = wallet.blocks.read().unwrap();
|
|
blk3_hash = blks[3].hash.clone();
|
|
blk4_hash = blks[4].hash.clone();
|
|
}
|
|
|
|
// This should result in an exception, because the "prevhash" is wrong
|
|
assert!(add_blocks(&wallet, 5, 2, blk3_hash).is_err(),
|
|
"Shouldn't be able to add because of invalid prev hash");
|
|
|
|
// Add with the proper prev hash
|
|
add_blocks(&wallet, 5, 2, blk4_hash).unwrap();
|
|
|
|
let blk6_hash;
|
|
{
|
|
let blks = wallet.blocks.read().unwrap();
|
|
blk6_hash = blks[6].hash.clone();
|
|
}
|
|
|
|
// Now do a Tx
|
|
let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap());
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
|
|
// Create a tx and send to address
|
|
const AMOUNT_SENT: u64 = 30000;
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&taddr, AMOUNT_SENT, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
let mut cb3 = FakeCompactBlock::new(7, blk6_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 7, 0);
|
|
|
|
// Make sure the Tx is in.
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT);
|
|
assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid));
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None);
|
|
|
|
// The sent tx should generate change
|
|
assert_eq!(txs[&sent_txid].notes.len(), 1);
|
|
assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT - AMOUNT_SENT - fee);
|
|
assert_eq!(txs[&sent_txid].notes[0].is_change, true);
|
|
assert_eq!(txs[&sent_txid].notes[0].spent, None);
|
|
assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None);
|
|
assert_eq!(txs[&sent_txid].notes[0].witnesses.len(), 1);
|
|
}
|
|
|
|
// Invalidate 3 blocks
|
|
assert_eq!(wallet.last_scanned_height(), 7);
|
|
assert_eq!(wallet.invalidate_block(5), 3);
|
|
assert_eq!(wallet.last_scanned_height(), 4);
|
|
|
|
// Make sure the orig Tx is there, but new Tx has disappeared
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
|
|
// Orig Tx is still there, since this is in block 0
|
|
// But now the spent tx is gone
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT);
|
|
assert_eq!(txs[&txid1].notes[0].spent, None);
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None);
|
|
|
|
// The sent tx is missing
|
|
assert!(txs.get(&sent_txid).is_none());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_t_derivation() {
|
|
let lc = LightClientConfig {
|
|
server: "0.0.0.0:0".parse().unwrap(),
|
|
chain_name: "main".to_string(),
|
|
sapling_activation_height: 0,
|
|
consensus_branch_id: "000000".to_string(),
|
|
anchor_offset: 1,
|
|
no_cert_verification: false,
|
|
data_dir: None,
|
|
};
|
|
|
|
let seed_phrase = Some("chimney better bulb horror rebuild whisper improve intact letter giraffe brave rib appear bulk aim burst snap salt hill sad merge tennis phrase raise".to_string());
|
|
|
|
let wallet = LightWallet::new(seed_phrase.clone(), &lc, 0, 0).unwrap();
|
|
|
|
// t: m/44'/141'/0'/0/i, checked against an independent BIP39/BIP32 derivation.
|
|
// z: ZIP32 m/32'/141'/0' as produced by this crate (snapshot).
|
|
let (taddr, pk) = &wallet.get_t_secret_keys()[0];
|
|
assert_eq!(taddr, "RUytX9weUJDFqmzZPh7iRm7n99ksh2HmXP");
|
|
assert_eq!(pk, "UvdRFABcSj8uXtzdVAb72ddtrKB69UCLg4ykGfkFMKRcvSozEwNe");
|
|
|
|
// Test a couple more
|
|
wallet.add_taddr();
|
|
let (taddr, pk) = &wallet.get_t_secret_keys()[1];
|
|
assert_eq!(taddr, "RMmVfFs82EvHEGny6zD3Hjf1C3Yz9jkQXf");
|
|
assert_eq!(pk, "UqzH1HqvD1qyy6xS4fMnowPnDJis9h5VEN2qSUD1TPHa7qJp5WfP");
|
|
|
|
let (zaddr, sk, _) = &wallet.get_z_private_keys()[0];
|
|
assert_eq!(zaddr, "zs1uqgg6nq8uct2wv5pszgw65ea4ft9z0kkcememm5qaspw8c2lgpulf78e46y46r23cz9cz48eqwe");
|
|
assert_eq!(sk, "secret-extended-key-main1qv6r09npqqqqpqphzfl70tss2ysdnsv8gjh8zxtk8wuuqd8k48re30nu7gdalh3llalq6mtefpv6w2efm4aklk6dfpe7udpnee08u69gc565mmfv7jcshf2t2llv3424q2kldfks7npww8rc8m2dah3tt67dv0cg4yqpxlsy8nmz65vl5h4wvw5nxtedry77s5hsn73t2lf0c3p6ps5ju959h8y9v03y2yk3vn3kynh6csls9zayqh5xup4l82d5yhwvgmelgvvyfxgcd25xa");
|
|
|
|
assert_eq!(seed_phrase, Some(wallet.get_seed_phrase()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_lock_unlock() {
|
|
const AMOUNT: u64 = 500000;
|
|
|
|
let (mut wallet, _, _) = get_test_wallet(AMOUNT);
|
|
let config = wallet.config.clone();
|
|
|
|
// Add some addresses
|
|
let zaddr0 = encode_payment_address(config.hrp_sapling_address(),
|
|
&wallet.zkeys.read().unwrap()[0].extfvk.default_address().unwrap().1);
|
|
let zaddr1 = wallet.add_zaddr(); // This is actually address at index 6
|
|
let zaddr2 = wallet.add_zaddr(); // This is actually address at index 7
|
|
|
|
let taddr0 = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
let taddr1 = wallet.add_taddr();
|
|
let taddr2 = wallet.add_taddr();
|
|
|
|
let seed = wallet.seed;
|
|
|
|
// Trying to lock a wallet that's not encrpyted is an error
|
|
assert!(wallet.lock().is_err());
|
|
|
|
// Encrypt the wallet
|
|
wallet.encrypt("somepassword".to_string()).unwrap();
|
|
|
|
// Encrypting an already encrypted wallet should fail
|
|
assert!(wallet.encrypt("somepassword".to_string()).is_err());
|
|
|
|
// Serialize a locked wallet
|
|
let mut serialized_data = vec![];
|
|
wallet.write(&mut serialized_data).expect("Serialize wallet");
|
|
|
|
// Should fail when there's a wrong password
|
|
assert!(wallet.unlock("differentpassword".to_string()).is_err());
|
|
|
|
// Properly unlock
|
|
wallet.unlock("somepassword".to_string()).unwrap();
|
|
|
|
assert_eq!(seed, wallet.seed);
|
|
{
|
|
let zkeys = wallet.zkeys.read().unwrap();
|
|
let tkeys = wallet.tkeys.read().unwrap();
|
|
assert_eq!(zkeys.len(), 8); // 3 zaddrs + 1 original + 4 extra HD addreses
|
|
assert_eq!(tkeys.len(), 3);
|
|
|
|
assert_eq!(zaddr0, encode_payment_address(config.hrp_sapling_address(),
|
|
&ExtendedFullViewingKey::from(zkeys[0].extsk.as_ref().unwrap()).default_address().unwrap().1));
|
|
assert_eq!(zaddr1, encode_payment_address(config.hrp_sapling_address(),
|
|
&ExtendedFullViewingKey::from(zkeys[6].extsk.as_ref().unwrap()).default_address().unwrap().1));
|
|
assert_eq!(zaddr2, encode_payment_address(config.hrp_sapling_address(),
|
|
&ExtendedFullViewingKey::from(zkeys[7].extsk.as_ref().unwrap()).default_address().unwrap().1));
|
|
|
|
assert_eq!(taddr0, wallet.address_from_sk(&tkeys[0].tkey.unwrap()));
|
|
assert_eq!(taddr1, wallet.address_from_sk(&tkeys[1].tkey.unwrap()));
|
|
assert_eq!(taddr2, wallet.address_from_sk(&tkeys[2].tkey.unwrap()));
|
|
}
|
|
|
|
// Unlocking an already unlocked wallet should fail
|
|
assert!(wallet.unlock("somepassword".to_string()).is_err());
|
|
|
|
// Trying to serialize a encrypted but unlocked wallet should fail
|
|
assert!(wallet.write(&mut vec![]).is_err());
|
|
|
|
// ...but if we lock it again, it should serialize
|
|
wallet.lock().unwrap();
|
|
wallet.write(&mut vec![]).expect("Serialize wallet");
|
|
|
|
// Locking an already locked wallet is an error
|
|
assert!(wallet.lock().is_err());
|
|
|
|
// Try from a deserialized, locked wallet
|
|
let mut wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap();
|
|
wallet2.unlock("somepassword".to_string()).unwrap();
|
|
|
|
assert_eq!(seed, wallet2.seed);
|
|
{
|
|
let zkeys = wallet2.zkeys.read().unwrap();
|
|
let tkeys = wallet2.tkeys.read().unwrap();
|
|
assert_eq!(zkeys.len(), 8);
|
|
assert_eq!(tkeys.len(), 3);
|
|
|
|
assert_eq!(zaddr0, encode_payment_address(wallet2.config.hrp_sapling_address(),
|
|
&ExtendedFullViewingKey::from(zkeys[0].extsk.as_ref().unwrap()).default_address().unwrap().1));
|
|
assert_eq!(zaddr1, encode_payment_address(wallet2.config.hrp_sapling_address(),
|
|
&ExtendedFullViewingKey::from(zkeys[6].extsk.as_ref().unwrap()).default_address().unwrap().1));
|
|
assert_eq!(zaddr2, encode_payment_address(wallet2.config.hrp_sapling_address(),
|
|
&ExtendedFullViewingKey::from(zkeys[7].extsk.as_ref().unwrap()).default_address().unwrap().1));
|
|
|
|
assert_eq!(taddr0, wallet2.address_from_sk(&tkeys[0].tkey.unwrap()));
|
|
assert_eq!(taddr1, wallet2.address_from_sk(&tkeys[1].tkey.unwrap()));
|
|
assert_eq!(taddr2, wallet2.address_from_sk(&tkeys[2].tkey.unwrap()));
|
|
}
|
|
|
|
// Remove encryption from a unlocked wallet should succeed
|
|
wallet2.remove_encryption("somepassword".to_string()).unwrap();
|
|
assert_eq!(seed, wallet2.seed);
|
|
|
|
// Now encrypt with a different password
|
|
wallet2.encrypt("newpassword".to_string()).unwrap();
|
|
assert_eq!([0u8; 32], wallet2.seed); // Seed is cleared out
|
|
|
|
// Locking should fail because it is already locked
|
|
assert!(wallet2.lock().is_err());
|
|
|
|
// The old password shouldn't work
|
|
assert!(wallet2.remove_encryption("somepassword".to_string()).is_err());
|
|
|
|
// Remove encryption with the right password
|
|
wallet2.remove_encryption("newpassword".to_string()).unwrap();
|
|
assert_eq!(seed, wallet2.seed);
|
|
|
|
// Unlocking a wallet without encryption is an error
|
|
assert!(wallet2.remove_encryption("newpassword".to_string()).is_err());
|
|
// Can't lock/unlock a wallet that's not encrypted
|
|
assert!(wallet2.lock().is_err());
|
|
assert!(wallet2.unlock("newpassword".to_string()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn test_invalid_bip39_t() {
|
|
// Passing a 32-byte seed to bip32 should fail.
|
|
let config = get_test_config();
|
|
LightWallet::get_taddr_from_bip39seed(&config, &[0u8; 32], 0);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn test_invalid_bip39_z() {
|
|
// Passing a 32-byte seed to bip32 should fail.
|
|
let config = get_test_config();
|
|
LightWallet::get_zaddr_from_bip39seed(&config, &[0u8; 32], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_scan_blocks() {
|
|
const AMOUNT: u64 = 500000;
|
|
let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT);
|
|
|
|
let prev_hash = add_blocks(&wallet, 2, 1, block_hash).unwrap();
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 3);
|
|
|
|
// Block fails to scan for bad encoding
|
|
assert_eq!(wallet.scan_block(&[0; 32]), Err(-1));
|
|
|
|
// Block is invalid height
|
|
let new_blk = FakeCompactBlock::new(4, prev_hash);
|
|
assert_eq!(wallet.scan_block(&new_blk.as_bytes()), Err(2));
|
|
|
|
// Block is right height, but invalid prev height (for reorgs)
|
|
let new_blk = FakeCompactBlock::new(2, BlockHash([0; 32]));
|
|
assert_eq!(wallet.scan_block(&new_blk.as_bytes()), Err(2));
|
|
|
|
// Block is right height, but invalid prev height (for reorgs)
|
|
let new_blk = FakeCompactBlock::new(3, BlockHash([0; 32]));
|
|
assert_eq!(wallet.scan_block(&new_blk.as_bytes()), Err(2));
|
|
|
|
// Then the rest add properly
|
|
let _ = add_blocks(&wallet, 3, 2, prev_hash).unwrap();
|
|
assert_eq!(wallet.blocks.read().unwrap().len(), 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_encrypted_zreceive() {
|
|
const AMOUNT1: u64 = 50000;
|
|
let password: String = "password".to_string();
|
|
|
|
let (mut wallet, txid1, block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32]));
|
|
let ext_address = encode_payment_address(wallet.config.hrp_sapling_address(),
|
|
&fvk.default_address().unwrap().1);
|
|
|
|
const AMOUNT_SENT: u64 = 20;
|
|
|
|
let outgoing_memo = "Outgoing Memo".to_string();
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
|
|
// Create a tx and send to address
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&ext_address, AMOUNT_SENT, Some(outgoing_memo.clone()))], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
// Second z address. Keys can't be added to an encrypted wallet, so derive it now.
|
|
let zaddr2 = wallet.add_zaddr(); // This is address number 6
|
|
|
|
// Now that we have the transaction, we'll encrypt the wallet
|
|
wallet.encrypt(password.clone()).unwrap();
|
|
|
|
// Scan the tx and make sure it gets added
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
|
|
// Now, full scan the Tx, which should populate the Outgoing Meta data
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
// Now this new Spent tx should be in, so the note should be marked confirmed spent
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid));
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None);
|
|
|
|
// The sent tx should generate change
|
|
assert_eq!(txs[&sent_txid].notes.len(), 1);
|
|
assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee);
|
|
assert_eq!(txs[&sent_txid].notes[0].is_change, true);
|
|
assert_eq!(txs[&sent_txid].notes[0].spent, None);
|
|
assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None);
|
|
|
|
// Outgoing Metadata
|
|
assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1);
|
|
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1);
|
|
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, ext_address);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].memo.to_utf8().unwrap().unwrap(), outgoing_memo);
|
|
}
|
|
|
|
// Trying to spend from a locked wallet is an error
|
|
assert!(wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&ext_address, AMOUNT_SENT, None)], &default_fee(), |_| Ok(' '.to_string())).is_err());
|
|
|
|
// unlock the wallet so we can spend to the second z address
|
|
wallet.unlock(password.clone()).unwrap();
|
|
|
|
// Second z address (derived before encrypting)
|
|
const ZAMOUNT2:u64 = 30;
|
|
let outgoing_memo2 = "Outgoing Memo2".to_string();
|
|
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&zaddr2, ZAMOUNT2, Some(outgoing_memo2.clone()))], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
// Now lock the wallet again
|
|
wallet.lock().unwrap();
|
|
|
|
let sent_tx2 = Transaction::read(&raw_tx[..]).unwrap();
|
|
let txid2 = sent_tx2.txid();
|
|
|
|
let mut cb4 = FakeCompactBlock::new(3, cb3.hash());
|
|
cb4.add_tx(&sent_tx2);
|
|
wallet.scan_block(&cb4.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx2, 3, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
let prev_change_value = AMOUNT1 - AMOUNT_SENT - fee;
|
|
|
|
// Change note from prev transaction is spent
|
|
assert_eq!(txs[&sent_txid].notes[0].note.value, prev_change_value);
|
|
assert_eq!(txs[&sent_txid].notes[0].is_change, true);
|
|
assert_eq!(txs[&sent_txid].notes[0].spent, Some(txid2));
|
|
|
|
// New change note. So find it.
|
|
let change_note = txs[&txid2].notes.iter().find(|n| n.is_change).unwrap();
|
|
|
|
// New incoming tx is present
|
|
assert_eq!(change_note.note.value, prev_change_value - (ZAMOUNT2+fee));
|
|
assert_eq!(change_note.spent, None);
|
|
assert_eq!(change_note.unconfirmed_spent, None);
|
|
|
|
// Find zaddr2
|
|
let zaddr2_note = txs[&txid2].notes.iter().find(|n| n.note.value == ZAMOUNT2).unwrap();
|
|
assert_eq!(zaddr2_note.account, 6);
|
|
assert_eq!(zaddr2_note.is_change, false);
|
|
assert_eq!(zaddr2_note.spent, None);
|
|
assert_eq!(zaddr2_note.unconfirmed_spent, None);
|
|
assert_eq!(LightWallet::memo_str(&zaddr2_note.memo), Some(outgoing_memo2));
|
|
}
|
|
}
|
|
|
|
|
|
#[test]
|
|
fn test_encrypted_treceive() {
|
|
const AMOUNT1: u64 = 50000;
|
|
let password: String = "password".to_string();
|
|
let (mut wallet, txid1, block_hash) = get_test_wallet(AMOUNT1);
|
|
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
|
|
let taddr = wallet.address_from_sk(&SecretKey::from_slice(&[1u8; 32]).unwrap());
|
|
const AMOUNT_SENT: u64 = 30;
|
|
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
|
|
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&taddr, AMOUNT_SENT, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
// Second t address. Keys can't be added to an encrypted wallet, so derive it now.
|
|
let taddr2 = wallet.add_taddr();
|
|
|
|
// Now that we have the transaction, we'll encrypt the wallet
|
|
wallet.encrypt(password.clone()).unwrap();
|
|
|
|
let sent_tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
let mut cb3 = FakeCompactBlock::new(2, block_hash);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
// Now this new Spent tx should be in, so the note should be marked confirmed spent
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid1].notes.len(), 1);
|
|
assert_eq!(txs[&txid1].notes[0].note.value, AMOUNT1);
|
|
assert_eq!(txs[&txid1].notes[0].spent, Some(sent_txid));
|
|
assert_eq!(txs[&txid1].notes[0].unconfirmed_spent, None);
|
|
|
|
// The sent tx should generate change
|
|
assert_eq!(txs[&sent_txid].notes.len(), 1);
|
|
assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - AMOUNT_SENT - fee);
|
|
assert_eq!(txs[&sent_txid].notes[0].is_change, true);
|
|
assert_eq!(txs[&sent_txid].notes[0].spent, None);
|
|
assert_eq!(txs[&sent_txid].notes[0].unconfirmed_spent, None);
|
|
|
|
// Outgoing Metadata
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata.len(), 1);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].address, taddr);
|
|
assert_eq!(txs[&sent_txid].outgoing_metadata[0].value, AMOUNT_SENT);
|
|
assert_eq!(txs[&sent_txid].total_shielded_value_spent, AMOUNT1);
|
|
}
|
|
|
|
// Trying to spend from a locked wallet is an error
|
|
assert!(wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&taddr, AMOUNT_SENT, None)], &default_fee(), |_| Ok(' '.to_string())).is_err());
|
|
|
|
// unlock the wallet so we can spend to the second z address
|
|
wallet.unlock(password.clone()).unwrap();
|
|
|
|
// Second t address (derived before encrypting)
|
|
const TAMOUNT2:u64 = 50;
|
|
|
|
let (_, raw_tx) = wallet.send_to_address(branch_id, &ss, &so, false,
|
|
vec![(&taddr2, TAMOUNT2, None)], &default_fee(), |_| Ok(' '.to_string())).unwrap();
|
|
|
|
// Now lock the wallet again
|
|
wallet.lock().unwrap();
|
|
|
|
let sent_tx2 = Transaction::read(&raw_tx[..]).unwrap();
|
|
let txid2 = sent_tx2.txid();
|
|
|
|
let mut cb4 = FakeCompactBlock::new(3, cb3.hash());
|
|
cb4.add_tx(&sent_tx2);
|
|
wallet.scan_block(&cb4.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx2, 3, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
let prev_change_value = AMOUNT1 - AMOUNT_SENT - fee;
|
|
|
|
// Change note from prev transaction is spent
|
|
assert_eq!(txs[&sent_txid].notes[0].note.value, prev_change_value);
|
|
assert_eq!(txs[&sent_txid].notes[0].is_change, true);
|
|
assert_eq!(txs[&sent_txid].notes[0].spent, Some(txid2));
|
|
|
|
// New change note. So find it.
|
|
let change_note = txs[&txid2].notes.iter().find(|n| n.is_change).unwrap();
|
|
|
|
// New incoming tx is present
|
|
assert_eq!(change_note.note.value, prev_change_value - (TAMOUNT2+fee));
|
|
assert_eq!(change_note.spent, None);
|
|
assert_eq!(change_note.unconfirmed_spent, None);
|
|
|
|
// Find taddr2
|
|
let utxo2 = txs[&txid2].utxos.iter().find(|u| u.value == TAMOUNT2).unwrap();
|
|
assert_eq!(txs[&txid2].utxos.len(), 1);
|
|
assert_eq!(utxo2.address, taddr2);
|
|
assert_eq!(utxo2.txid, txid2);
|
|
assert_eq!(utxo2.spent, None);
|
|
assert_eq!(utxo2.unconfirmed_spent, None);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// g1 (C249): note addresses come from the stored pk_d, the z-balance address filters compare
|
|
// (diversifier, pk_d), and do_balance / do_list_notes use z_balances_by_address /
|
|
// spendable_zaddress_set. All of it must be exactly equivalent to the pre-change code. The old_*
|
|
// functions below are verbatim copies of the replaced bodies, taking the wallet as a parameter.
|
|
// ---------------------------------------------------------------------------------------------
|
|
|
|
use std::collections::HashSet;
|
|
use super::{SaplingNoteData, WalletTx, IncomingTxMetadata};
|
|
|
|
fn old_note_address(hrp: &str, note: &SaplingNoteData) -> Option<String> {
|
|
match note.extfvk.fvk.vk.into_payment_address(note.diversifier, &JUBJUB) {
|
|
Some(pa) => Some(encode_payment_address(hrp, &pa)),
|
|
None => None
|
|
}
|
|
}
|
|
|
|
fn old_zbalance(w: &LightWallet, addr: Option<String>) -> u64 {
|
|
let unconfirmed_balance = w.unconfirmed_zbalance(addr.clone());
|
|
|
|
let confirmed_balance = w.txs.read().unwrap()
|
|
.values()
|
|
.map(|tx| {
|
|
tx.notes.iter()
|
|
.filter(|nd| {
|
|
match addr.as_ref() {
|
|
Some(a) => *a == encode_payment_address(
|
|
w.config.hrp_sapling_address(),
|
|
&nd.extfvk.fvk.vk
|
|
.into_payment_address(nd.diversifier, &JUBJUB).unwrap()
|
|
),
|
|
None => true
|
|
}
|
|
})
|
|
.map(|nd| {
|
|
if nd.spent.is_none() && nd.unconfirmed_spent.is_none() {
|
|
nd.note.value
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
.sum::<u64>()
|
|
})
|
|
.sum::<u64>();
|
|
|
|
confirmed_balance + unconfirmed_balance
|
|
}
|
|
|
|
fn old_verified_zbalance(w: &LightWallet, addr: Option<String>) -> u64 {
|
|
let anchor_height = match w.get_target_height_and_anchor_offset() {
|
|
Some((height, anchor_offset)) => height - anchor_offset as u32 ,
|
|
None => return 0,
|
|
};
|
|
|
|
w.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| {
|
|
match addr.as_ref() {
|
|
Some(a) => *a == encode_payment_address(
|
|
w.config.hrp_sapling_address(),
|
|
&nd.extfvk.fvk.vk
|
|
.into_payment_address(nd.diversifier, &JUBJUB).unwrap()
|
|
),
|
|
None => true
|
|
}
|
|
})
|
|
.map(|nd| nd.note.value)
|
|
.sum::<u64>()
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
.sum::<u64>()
|
|
}
|
|
|
|
fn old_spendable_zbalance(w: &LightWallet, addr: Option<String>) -> u64 {
|
|
let anchor_height = w.get_anchor_height();
|
|
|
|
w.txs
|
|
.read()
|
|
.unwrap()
|
|
.values()
|
|
.map(|tx| {
|
|
if tx.block as u32 <= anchor_height {
|
|
tx.notes
|
|
.iter()
|
|
.filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none())
|
|
.filter(|nd| {
|
|
// Check to see if we have this note's spending key.
|
|
w.have_spendingkey_for_extfvk(&nd.extfvk)
|
|
})
|
|
.filter(|nd| {
|
|
match addr.as_ref() {
|
|
Some(a) => *a == encode_payment_address(
|
|
w.config.hrp_sapling_address(),
|
|
&nd.extfvk.fvk.vk
|
|
.into_payment_address(nd.diversifier, &JUBJUB).unwrap()
|
|
),
|
|
None => true
|
|
}
|
|
})
|
|
.map(|nd| nd.note.value)
|
|
.sum::<u64>()
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
.sum::<u64>() as u64
|
|
}
|
|
|
|
// The old do_list_notes construction of the spendable-address set.
|
|
fn old_spendable_address_set(w: &LightWallet) -> HashSet<String> {
|
|
w.get_all_zaddresses().iter()
|
|
.filter(|address| w.have_spending_key_for_zaddress(address))
|
|
.map(|address| address.clone())
|
|
.collect()
|
|
}
|
|
|
|
/// Deterministic xorshift64, so the shape of the g1 chains is reproducible (note randomness and
|
|
/// txids still come from OsRng).
|
|
struct G1Rng(u64);
|
|
|
|
impl G1Rng {
|
|
fn next(&mut self) -> u64 {
|
|
let mut x = self.0;
|
|
x ^= x << 13;
|
|
x ^= x >> 7;
|
|
x ^= x << 17;
|
|
self.0 = x;
|
|
x
|
|
}
|
|
|
|
fn below(&mut self, n: u64) -> u64 {
|
|
self.next() % n
|
|
}
|
|
}
|
|
|
|
/// A diversified (non-default) address of `extfvk`.
|
|
fn g1_diversified_address(extfvk: &ExtendedFullViewingKey) -> PaymentAddress<Bls12> {
|
|
let (mut j, _) = extfvk.default_address().unwrap();
|
|
j.increment().unwrap();
|
|
extfvk.address(j).unwrap().1
|
|
}
|
|
|
|
/// Minimal BIP-173 bech32 encoder, only used to build syntactically valid junk addresses.
|
|
fn g1_bech32(hrp: &str, data: &[u8]) -> String {
|
|
const CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
const GEN: [u32; 5] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
|
|
let mut five: Vec<u8> = vec![];
|
|
let mut acc: u32 = 0;
|
|
let mut bits: u32 = 0;
|
|
for b in data {
|
|
acc = (acc << 8) | (*b as u32);
|
|
bits += 8;
|
|
while bits >= 5 {
|
|
bits -= 5;
|
|
five.push(((acc >> bits) & 31) as u8);
|
|
}
|
|
acc &= (1 << bits) - 1;
|
|
}
|
|
if bits > 0 {
|
|
five.push(((acc << (5 - bits)) & 31) as u8);
|
|
}
|
|
|
|
let mut values: Vec<u8> = hrp.bytes().map(|c| c >> 5).collect();
|
|
values.push(0);
|
|
values.extend(hrp.bytes().map(|c| c & 31));
|
|
values.extend(five.iter().cloned());
|
|
values.extend([0u8; 6].iter().cloned());
|
|
let mut chk: u32 = 1;
|
|
for v in values {
|
|
let top = chk >> 25;
|
|
chk = ((chk & 0x1ffffff) << 5) ^ (v as u32);
|
|
for (i, g) in GEN.iter().enumerate() {
|
|
if (top >> i) & 1 == 1 {
|
|
chk ^= *g;
|
|
}
|
|
}
|
|
}
|
|
let polymod = chk ^ 1;
|
|
|
|
let mut s = String::from(hrp);
|
|
s.push('1');
|
|
for d in five {
|
|
s.push(CHARSET[d as usize] as char);
|
|
}
|
|
for i in 0..6 {
|
|
s.push(CHARSET[((polymod >> (5 * (5 - i))) & 31) as usize] as char);
|
|
}
|
|
s
|
|
}
|
|
|
|
struct G1Wallet {
|
|
wallet: LightWallet,
|
|
keys: Vec<ExtendedFullViewingKey>, // the 8 keys notes were paid to (6 HD, imported vk, imported sk)
|
|
diversified: Vec<String>, // diversified addresses of our keys that received notes
|
|
foreign: String, // default address of a key that is not in the wallet
|
|
vk_address: String, // the imported view-only key's address
|
|
sk_address: String, // the imported spending key's address
|
|
}
|
|
|
|
/// 6 HD keys, another wallet's key imported view-only and an imported spending key. Then `blocks`
|
|
/// blocks of payments chosen by `seed`: random key and value (zero-value and dust included),
|
|
/// foreign outputs, compact spends with change, and 3 notes paid to diversified addresses. The
|
|
/// view-only key's notes are never spent (so verified != spendable), and the tip block always pays
|
|
/// us (so anchor offsets and the spendable "- 1" matter).
|
|
fn g1_build(seed: u64, blocks: i32) -> G1Wallet {
|
|
use zcash_client_backend::encoding::{encode_extended_full_viewing_key, encode_extended_spending_key};
|
|
|
|
let config = get_test_config();
|
|
let hrp = config.hrp_sapling_address().to_string();
|
|
let mut rng = G1Rng(seed | 1);
|
|
|
|
let mut wallet = LightWallet::new(None, &config, 0, 0).unwrap();
|
|
for _ in 0..5 {
|
|
wallet.add_zaddr();
|
|
}
|
|
|
|
let other = LightWallet::new(None, &config, 0, 0).unwrap();
|
|
let vk_extfvk = other.zkeys.read().unwrap()[0].extfvk.clone();
|
|
let vk_address = wallet.add_imported_vk(
|
|
encode_extended_full_viewing_key(config.hrp_sapling_viewing_key(), &vk_extfvk), 0);
|
|
assert!(vk_address.starts_with(&hrp), "{}", vk_address);
|
|
|
|
let mut sk_seed = [0x5au8; 32];
|
|
sk_seed[..8].copy_from_slice(&seed.to_le_bytes());
|
|
let extsk = ExtendedSpendingKey::master(&sk_seed);
|
|
let sk_address = wallet.add_imported_sk(
|
|
encode_extended_spending_key(config.hrp_sapling_private_key(), &extsk), 0);
|
|
assert!(sk_address.starts_with(&hrp), "{}", sk_address);
|
|
|
|
let foreign_extfvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[7u8; 32]));
|
|
let foreign_pa = foreign_extfvk.default_address().unwrap().1;
|
|
let foreign = encode_payment_address(&hrp, &foreign_pa);
|
|
|
|
let keys: Vec<ExtendedFullViewingKey> = wallet.zkeys.read().unwrap().iter()
|
|
.map(|zk| zk.extfvk.clone()).collect();
|
|
assert_eq!(keys.len(), 8);
|
|
assert!(keys[6] == vk_extfvk);
|
|
|
|
let diversified_heights = [blocks / 6, blocks / 2, blocks - 3];
|
|
let mut diversified: Vec<String> = vec![];
|
|
let mut prev = BlockHash([0; 32]);
|
|
for h in 0..blocks {
|
|
let mut cb = FakeCompactBlock::new(h, prev);
|
|
|
|
// Sometimes a compact spend (with change) of an unspent, non-zero, spendable-key note.
|
|
if h > 2 && rng.below(4) == 0 {
|
|
let candidate = {
|
|
let txs = wallet.txs.read().unwrap();
|
|
let mut c: Vec<(i32, u64, Vec<u8>, ExtendedFullViewingKey)> = txs.values()
|
|
.flat_map(|tx| tx.notes.iter().map(move |nd| (tx.block, nd)))
|
|
.filter(|(_, nd)| nd.spent.is_none() && nd.note.value > 0 && nd.extfvk != vk_extfvk
|
|
&& !diversified.contains(&old_note_address(&hrp, nd).unwrap()))
|
|
.map(|(b, nd)| (b, nd.note.value, nd.nullifier.to_vec(), nd.extfvk.clone()))
|
|
.collect();
|
|
// HashMap order is per instance; sort so the pick depends only on the chain.
|
|
c.sort_by(|x, y| (x.0, x.1, &x.2).cmp(&(y.0, y.1, &y.2)));
|
|
if c.is_empty() {
|
|
None
|
|
} else {
|
|
let i = rng.below(c.len() as u64) as usize;
|
|
Some(c.swap_remove(i))
|
|
}
|
|
};
|
|
if let Some((_, value, nf, extfvk)) = candidate {
|
|
let sent = rng.below(value + 1);
|
|
cb.add_tx_spending((nf, value), extfvk, foreign_pa.clone(), sent);
|
|
}
|
|
}
|
|
|
|
// 0-2 incoming payments to random keys (always at least one in the last 4 blocks).
|
|
let n = if h >= blocks - 4 { 1 + rng.below(2) } else { rng.below(3) };
|
|
for _ in 0..n {
|
|
let k = rng.below(keys.len() as u64) as usize;
|
|
let value = match rng.below(10) {
|
|
0 => 0, // zero-value (HushChat-style) note
|
|
1 | 2 => 1 + rng.below(1000), // dust
|
|
_ => 1 + rng.below(1_000_000_000),
|
|
};
|
|
cb.add_tx_paying(keys[k].clone(), value);
|
|
}
|
|
if h == 0 {
|
|
cb.add_tx_paying(vk_extfvk.clone(), 777_777);
|
|
}
|
|
if h == blocks - 1 {
|
|
cb.add_tx_paying(keys[0].clone(), 333_333);
|
|
}
|
|
|
|
// A foreign output now and then.
|
|
if rng.below(3) == 0 {
|
|
cb.add_tx_paying(foreign_extfvk.clone(), 1 + rng.below(1000));
|
|
}
|
|
|
|
// 3 notes paid to diversified addresses of our keys.
|
|
if diversified_heights.contains(&h) {
|
|
let k = rng.below(keys.len() as u64) as usize;
|
|
let to = g1_diversified_address(&keys[k]);
|
|
assert!(to != keys[k].default_address().unwrap().1);
|
|
diversified.push(encode_payment_address(&hrp, &to));
|
|
cb.add_tx_paying_to(keys[k].clone(), to, 10_000 + rng.below(1_000_000));
|
|
}
|
|
|
|
wallet.scan_block(&cb.as_bytes()).unwrap();
|
|
prev = cb.hash();
|
|
}
|
|
|
|
G1Wallet { wallet, keys, diversified, foreign, vk_address, sk_address }
|
|
}
|
|
|
|
/// Marks about a third of the unspent notes spent or pending-spent (deterministic per seed), except
|
|
/// the view-only key's notes and the tip block's notes.
|
|
fn g1_mark_spent(g: &G1Wallet, seed: u64) {
|
|
let mut rng = G1Rng(seed | 1);
|
|
let tip = g.wallet.last_scanned_height();
|
|
let mut txs = g.wallet.txs.write().unwrap();
|
|
let mut ids: Vec<(i32, u64, TxId)> = txs.values()
|
|
.map(|tx| (tx.block, tx.notes.iter().map(|nd| nd.note.value).sum::<u64>(), tx.txid))
|
|
.collect();
|
|
ids.sort_by(|x, y| (x.0, x.1, x.2 .0).cmp(&(y.0, y.1, y.2 .0)));
|
|
for (block, _, txid) in ids {
|
|
if block == tip {
|
|
continue;
|
|
}
|
|
for nd in txs.get_mut(&txid).unwrap().notes.iter_mut() {
|
|
if nd.spent.is_some() || nd.extfvk == g.keys[6] {
|
|
continue;
|
|
}
|
|
match rng.below(6) {
|
|
0 => nd.spent = Some(TxId([0xab; 32])),
|
|
1 => nd.unconfirmed_spent = Some(TxId([0xcd; 32])),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Adds an incoming mempool tx paying two of our addresses and a diversified one, so the
|
|
/// unconfirmed term of zbalance is non-zero.
|
|
fn g1_add_incoming_mempool(g: &G1Wallet) {
|
|
let hrp = g.wallet.config.hrp_sapling_address().to_string();
|
|
let txid = TxId([0x11; 32]);
|
|
let mut wtx = WalletTx::new(g.wallet.last_scanned_height() + 1, 0, &txid);
|
|
for (address, value) in vec![
|
|
(encode_payment_address(&hrp, &g.keys[1].default_address().unwrap().1), 4242u64),
|
|
(g.sk_address.clone(), 17),
|
|
(g.diversified[0].clone(), 99),
|
|
] {
|
|
wtx.incoming_metadata.push(IncomingTxMetadata {
|
|
address,
|
|
value,
|
|
memo: Memo::default(),
|
|
incoming_mempool: true,
|
|
});
|
|
}
|
|
g.wallet.incoming_mempool_txs.write().unwrap().insert(txid, vec![wtx]);
|
|
}
|
|
|
|
/// Every address-filter input of tests 2 and 3: None, the wallet's addresses, the diversified
|
|
/// addresses, and junk that must keep the old exact string compare (never decoded).
|
|
fn g1_inputs(g: &G1Wallet) -> Vec<Option<String>> {
|
|
let hrp = g.wallet.config.hrp_sapling_address().to_string();
|
|
let mut inputs: Vec<Option<String>> = vec![None];
|
|
let all = g.wallet.get_all_zaddresses();
|
|
inputs.extend(all.iter().cloned().map(Some));
|
|
inputs.extend(g.diversified.iter().cloned().map(Some));
|
|
|
|
let valid = all[1].clone();
|
|
let mut last_changed = valid.clone();
|
|
let last = last_changed.pop().unwrap();
|
|
last_changed.push(if last == 'q' { 'p' } else { 'q' });
|
|
|
|
// The bech32 helper is exact: it reproduces encode_payment_address on a real address.
|
|
let pa = g.keys[2].default_address().unwrap().1;
|
|
let mut payload = pa.diversifier.0.to_vec();
|
|
pa.pk_d.write(&mut payload).unwrap();
|
|
assert_eq!(g1_bech32(&hrp, &payload), encode_payment_address(&hrp, &pa));
|
|
|
|
inputs.extend(vec![
|
|
String::new(),
|
|
valid.to_uppercase(),
|
|
last_changed,
|
|
g.foreign.clone(),
|
|
"z".repeat(300),
|
|
g1_bech32(&hrp, &[1, 2, 3, 4, 5]), // valid bech32, short payload
|
|
g1_bech32(&hrp, &payload[..42]), // valid bech32, one byte short
|
|
g1_bech32("ztestsapling", &payload), // our key under the testnet hrp
|
|
format!("{} ", valid),
|
|
].into_iter().map(Some));
|
|
inputs
|
|
}
|
|
|
|
fn g1_assert_filters_match_old(w: &LightWallet, inputs: &[Option<String>], what: &str) {
|
|
for a in inputs {
|
|
assert_eq!(w.zbalance(a.clone()), old_zbalance(w, a.clone()), "{}: zbalance({:?})", what, a);
|
|
assert_eq!(w.verified_zbalance(a.clone()), old_verified_zbalance(w, a.clone()),
|
|
"{}: verified_zbalance({:?})", what, a);
|
|
assert_eq!(w.spendable_zbalance(a.clone()), old_spendable_zbalance(w, a.clone()),
|
|
"{}: spendable_zbalance({:?})", what, a);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_note_address_pk_d_matches_derivation() {
|
|
for seed in &[0x9e3779b97f4a7c15u64, 0x0123456789abcdef] {
|
|
let g = g1_build(*seed, 40);
|
|
let config = g.wallet.config.clone();
|
|
let hrp = config.hrp_sapling_address().to_string();
|
|
|
|
let check = |w: &LightWallet, what: &str| -> usize {
|
|
let txs = w.txs.read().unwrap();
|
|
let mut n = 0;
|
|
let mut seen: HashSet<String> = HashSet::new();
|
|
for tx in txs.values() {
|
|
for nd in tx.notes.iter() {
|
|
let new = LightWallet::note_address(&hrp, nd);
|
|
assert!(new.is_some(), "{}: no address", what);
|
|
assert_eq!(new, old_note_address(&hrp, nd), "{}: note address differs", what);
|
|
seen.insert(new.unwrap());
|
|
n += 1;
|
|
}
|
|
}
|
|
for d in g.diversified.iter() {
|
|
assert!(seen.contains(d), "{}: diversified address {} not derived", what, d);
|
|
}
|
|
n
|
|
};
|
|
|
|
let n = check(&g.wallet, "scanned");
|
|
assert!(n >= 30, "only {} notes", n);
|
|
let all = g.wallet.get_all_zaddresses();
|
|
for d in g.diversified.iter() {
|
|
assert!(!all.contains(d));
|
|
}
|
|
|
|
// Notes rebuilt by SaplingNoteData::read (data.rs:158) give the same addresses, and the
|
|
// file format is unchanged: write -> read -> write is byte-identical.
|
|
let mut first = vec![];
|
|
g.wallet.write(&mut first).unwrap();
|
|
let reread = LightWallet::read(&first[..], &config).unwrap();
|
|
assert_eq!(check(&reread, "reread"), n);
|
|
let mut second = vec![];
|
|
reread.write(&mut second).unwrap();
|
|
assert!(first == second, "write -> read -> write changed the wallet bytes");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_zbalance_filters_match_old() {
|
|
for seed in &[0x2545f4914f6cdd1du64, 0x00c0ffee12345678] {
|
|
let g = g1_build(*seed, 40);
|
|
g1_mark_spent(&g, *seed ^ 0xff);
|
|
g1_add_incoming_mempool(&g);
|
|
let inputs = g1_inputs(&g);
|
|
|
|
// Non-vacuous: pending and confirmed spends exist, view-only notes make verified !=
|
|
// spendable, and the junk inputs match nothing.
|
|
{
|
|
let txs = g.wallet.txs.read().unwrap();
|
|
let notes = txs.values().flat_map(|tx| tx.notes.iter());
|
|
assert!(notes.clone().any(|nd| nd.spent.is_some()));
|
|
assert!(notes.clone().any(|nd| nd.unconfirmed_spent.is_some()));
|
|
}
|
|
assert!(g.wallet.verified_zbalance(None) > g.wallet.spendable_zbalance(None));
|
|
assert!(g.wallet.verified_zbalance(Some(g.vk_address.clone())) >= 777_777);
|
|
assert_eq!(g.wallet.spendable_zbalance(Some(g.vk_address.clone())), 0);
|
|
assert_eq!(g.wallet.zbalance(Some(g.foreign.clone())), 0);
|
|
g1_assert_filters_match_old(&g.wallet, &inputs, "offset 0");
|
|
|
|
// Same wallet read back with anchor_offset 3. read() drops the pending spends, so mark
|
|
// again.
|
|
let mut bytes = vec![];
|
|
g.wallet.write(&mut bytes).unwrap();
|
|
let mut config3 = g.wallet.config.clone();
|
|
config3.anchor_offset = 3;
|
|
let w3 = LightWallet::read(&bytes[..], &config3).unwrap();
|
|
g1_assert_filters_match_old(&w3, &inputs, "offset 3, reread");
|
|
// The tip block's 333_333 note is above the offset-3 anchor.
|
|
assert!(w3.verified_zbalance(None) + 333_333 <= w3.zbalance(None) - w3.unconfirmed_zbalance(None));
|
|
let g3 = G1Wallet { wallet: w3, keys: g.keys.clone(), diversified: g.diversified.clone(),
|
|
foreign: g.foreign.clone(), vk_address: g.vk_address.clone(),
|
|
sk_address: g.sk_address.clone() };
|
|
g1_mark_spent(&g3, *seed ^ 0x5555);
|
|
g1_add_incoming_mempool(&g3);
|
|
g1_assert_filters_match_old(&g3.wallet, &inputs, "offset 3, re-marked");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_z_balances_by_address_matches_per_address_calls() {
|
|
for seed in &[0x6a09e667f3bcc908u64, 0x3c6ef372fe94f82b] {
|
|
let g = g1_build(*seed, 40);
|
|
g1_add_incoming_mempool(&g);
|
|
let w = &g.wallet;
|
|
|
|
let by_address = w.z_balances_by_address();
|
|
let all = w.get_all_zaddresses();
|
|
assert_eq!(by_address.iter().map(|t| t.0.clone()).collect::<Vec<String>>(), all);
|
|
assert_eq!(all.iter().cloned().collect::<HashSet<String>>().len(), all.len());
|
|
|
|
let mut confirmed_listed: u64 = 0;
|
|
for (a, zbalance, unconfirmed, verified, spendable) in by_address.iter() {
|
|
let s = Some(a.clone());
|
|
assert_eq!(*zbalance, w.zbalance(s.clone()), "zbalance {}", a);
|
|
assert_eq!(*unconfirmed, w.unconfirmed_zbalance(s.clone()), "unconfirmed {}", a);
|
|
assert_eq!(*verified, w.verified_zbalance(s.clone()), "verified {}", a);
|
|
assert_eq!(*spendable, w.spendable_zbalance(s.clone()), "spendable {}", a);
|
|
assert_eq!(*zbalance, old_zbalance(w, s.clone()), "old zbalance {}", a);
|
|
assert_eq!(*verified, old_verified_zbalance(w, s.clone()), "old verified {}", a);
|
|
assert_eq!(*spendable, old_spendable_zbalance(w, s.clone()), "old spendable {}", a);
|
|
confirmed_listed += zbalance - unconfirmed;
|
|
}
|
|
assert!(by_address.iter().any(|t| t.2 > 0), "no unconfirmed term exercised");
|
|
|
|
// Notes paid to diversified addresses belong to no listed address, as before.
|
|
let hrp = w.config.hrp_sapling_address().to_string();
|
|
let diversified_unspent: u64 = w.txs.read().unwrap().values()
|
|
.flat_map(|tx| tx.notes.iter())
|
|
.filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none()
|
|
&& g.diversified.contains(&old_note_address(&hrp, nd).unwrap()))
|
|
.map(|nd| nd.note.value)
|
|
.sum();
|
|
assert!(diversified_unspent > 0);
|
|
let confirmed_all = w.zbalance(None) - w.unconfirmed_zbalance(None);
|
|
assert_eq!(confirmed_listed + diversified_unspent, confirmed_all);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_spendable_zaddress_set_matches_old() {
|
|
let g = g1_build(0x510e527fade682d1, 12);
|
|
let vk_address = g.vk_address.clone();
|
|
let sk_address = g.sk_address.clone();
|
|
let mut w = g.wallet;
|
|
|
|
let check = |w: &LightWallet, what: &str| {
|
|
let set = w.spendable_zaddress_set();
|
|
assert_eq!(set, old_spendable_address_set(w), "{}", what);
|
|
assert!(!set.contains(&vk_address), "{}: view-only key counted as spendable", what);
|
|
assert!(set.contains(&sk_address), "{}: imported spending key missing", what);
|
|
assert_eq!(set.len(), w.get_all_zaddresses().len() - 1, "{}", what);
|
|
};
|
|
|
|
check(&w, "plain");
|
|
w.encrypt("g1 password".to_string()).unwrap();
|
|
check(&w, "encrypted + locked");
|
|
w.unlock("g1 password".to_string()).unwrap();
|
|
check(&w, "unlocked");
|
|
w.remove_encryption("g1 password".to_string()).unwrap();
|
|
check(&w, "decrypted");
|
|
}
|
|
|
|
#[test]
|
|
fn test_balances_no_deadlock_with_zkeys_writer() {
|
|
use std::sync::{Arc, mpsc, atomic::{AtomicBool, Ordering}};
|
|
use std::time::Duration;
|
|
|
|
let wallet = Arc::new(LightWallet::new(None, &get_test_config(), 0, 0).unwrap());
|
|
for _ in 0..5 {
|
|
wallet.add_zaddr();
|
|
}
|
|
let writer_done = Arc::new(AtomicBool::new(false));
|
|
|
|
// Reader: the do_balance and send-path balance calls, back to back.
|
|
let (rtx, rrx) = mpsc::channel();
|
|
let reader = {
|
|
let w = wallet.clone();
|
|
let done = writer_done.clone();
|
|
std::thread::spawn(move || {
|
|
let mut rounds = 0u64;
|
|
let mut seen = 0u64;
|
|
while rounds < 300 || (!done.load(Ordering::SeqCst) && rounds < 100_000) {
|
|
seen += w.z_balances_by_address().len() as u64;
|
|
seen += w.spendable_zbalance(None) % 2;
|
|
rounds += 1;
|
|
}
|
|
rtx.send((rounds, seen)).unwrap();
|
|
})
|
|
};
|
|
|
|
// Writer: add_zaddr (zkeys.write) interleaved with scan_block of blocks paying the newest HD
|
|
// address, so ensure_hd_zaddresses takes zkeys.write while scan_block holds txs.write.
|
|
let (wtx, wrx) = mpsc::channel();
|
|
let writer = {
|
|
let w = wallet.clone();
|
|
let done = writer_done.clone();
|
|
std::thread::spawn(move || {
|
|
let mut prev = BlockHash([0; 32]);
|
|
for i in 0..100 {
|
|
w.add_zaddr();
|
|
let newest = w.zkeys.read().unwrap().last().unwrap().extfvk.clone();
|
|
let mut cb = FakeCompactBlock::new(i, prev);
|
|
cb.add_tx_paying(newest, 1000 + i as u64);
|
|
w.scan_block(&cb.as_bytes()).unwrap();
|
|
prev = cb.hash();
|
|
}
|
|
done.store(true, Ordering::SeqCst);
|
|
wtx.send(w.zkeys.read().unwrap().len()).unwrap();
|
|
})
|
|
};
|
|
|
|
let nkeys = wrx.recv_timeout(Duration::from_secs(120))
|
|
.expect("add_zaddr/scan_block did not finish within 120 s (lock-order deadlock?)");
|
|
let (rounds, _) = rrx.recv_timeout(Duration::from_secs(120))
|
|
.expect("z_balances_by_address/spendable_zbalance did not finish within 120 s (deadlock?)");
|
|
writer.join().unwrap();
|
|
reader.join().unwrap();
|
|
|
|
// 6 + 100 add_zaddr + 5 per block from the gap rule.
|
|
assert_eq!(nkeys, 6 + 100 + 5 * 100);
|
|
assert!(rounds >= 300);
|
|
assert_eq!(wallet.zbalance(None), (0..100u64).map(|i| 1000 + i).sum::<u64>());
|
|
assert_eq!(wallet.z_balances_by_address().len(), nkeys);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// g4 (C246): send_to_address picks notes with note_selection::select_spends: the smallest single
|
|
// covering note, else the fewest notes (largest first); zero-value notes are never spent; at most
|
|
// MAX_SPENDS_PER_TX spends; the keyed UTXOs only reduce what the notes must cover when the notes
|
|
// cannot cover the target alone, and then (integration O4) at least the smallest non-zero note is
|
|
// still spent so the change stays `is_change`. Notes are identified by their (distinct) values.
|
|
// ---------------------------------------------------------------------------------------------
|
|
|
|
/// A wallet with one note per value, all to zkeys[0]'s default address, in block 0, plus an empty
|
|
/// block 1 (anchor offset 0: every note is spendable). Returns the wallet and the tip hash.
|
|
fn g4_wallet(values: &[u64]) -> (LightWallet, BlockHash) {
|
|
let config = get_test_config();
|
|
let wallet = LightWallet::new(None, &config, 0, 0).unwrap();
|
|
let extfvk = wallet.zkeys.read().unwrap()[0].extfvk.clone();
|
|
|
|
let mut cb1 = FakeCompactBlock::new(0, BlockHash([0; 32]));
|
|
for &v in values {
|
|
cb1.add_tx_paying(extfvk.clone(), v);
|
|
}
|
|
wallet.scan_block(&cb1.as_bytes()).unwrap();
|
|
let cb2 = FakeCompactBlock::new(1, cb1.hash());
|
|
wallet.scan_block(&cb2.as_bytes()).unwrap();
|
|
|
|
let mut got: Vec<u64> = wallet.txs.read().unwrap().values()
|
|
.flat_map(|t| t.notes.iter().map(|nd| nd.note.value)).collect();
|
|
got.sort();
|
|
let mut want = values.to_vec();
|
|
want.sort();
|
|
assert_eq!(got, want, "every paid note is in the wallet");
|
|
(wallet, cb2.hash())
|
|
}
|
|
|
|
/// Give the wallet's first t-address one UTXO of `value` (keyed: the wallet holds its secret key).
|
|
fn g4_add_utxo(wallet: &LightWallet, value: u64) -> TxId {
|
|
let secp = Secp256k1::new();
|
|
let pk = PublicKey::from_secret_key(&secp, &wallet.tkeys.read().unwrap()[0].tkey.unwrap());
|
|
let mut tx = FakeTransaction::new(&mut OsRng);
|
|
tx.add_t_output(&pk, value);
|
|
let txid = tx.get_tx().txid();
|
|
wallet.scan_full_tx(&tx.get_tx(), 1, 0);
|
|
assert_eq!(wallet.tbalance(None), value);
|
|
txid
|
|
}
|
|
|
|
fn g4_ext_address(wallet: &LightWallet) -> String {
|
|
let fvk = ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[1u8; 32]));
|
|
encode_payment_address(wallet.config.hrp_sapling_address(), &fvk.default_address().unwrap().1)
|
|
}
|
|
|
|
fn g4_send(wallet: &LightWallet, tos: Vec<(&str, u64, Option<String>)>) -> Result<Transaction, String> {
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
wallet.send_to_address(branch_id, &ss, &so, false, tos, &default_fee(), |_| Ok(' '.to_string()))
|
|
.map(|(_, raw)| Transaction::read(&raw[..]).unwrap())
|
|
}
|
|
|
|
/// The values of the notes `tx` spends, sorted. Also checks that the nullifiers are distinct, are
|
|
/// all ours, and that `unconfirmed_spent` is set on exactly those notes.
|
|
fn g4_spent_values(wallet: &LightWallet, tx: &Transaction) -> Vec<u64> {
|
|
let spent: HashSet<[u8; 32]> = tx.shielded_spends.iter().map(|s| s.nullifier).collect();
|
|
assert_eq!(spent.len(), tx.shielded_spends.len(), "a nullifier is spent twice in one tx");
|
|
let txs = wallet.txs.read().unwrap();
|
|
let mut values = vec![];
|
|
for nd in txs.values().flat_map(|t| t.notes.iter()) {
|
|
let marked = nd.unconfirmed_spent == Some(tx.txid());
|
|
assert_eq!(marked, spent.contains(&nd.nullifier),
|
|
"unconfirmed_spent must be set on exactly the spent notes (note value {})", nd.note.value);
|
|
assert!(nd.unconfirmed_spent.is_none() || marked, "a note is pending for another tx");
|
|
if marked {
|
|
values.push(nd.note.value);
|
|
}
|
|
}
|
|
assert_eq!(values.len(), spent.len(), "every spent nullifier is one of our notes");
|
|
values.sort();
|
|
values
|
|
}
|
|
|
|
fn g4_zero_notes_untouched(wallet: &LightWallet) {
|
|
let txs = wallet.txs.read().unwrap();
|
|
for nd in txs.values().flat_map(|t| t.notes.iter()).filter(|nd| nd.note.value == 0) {
|
|
assert_eq!(nd.unconfirmed_spent, None, "a zero-value note was spent");
|
|
assert_eq!(nd.spent, None, "a zero-value note was spent");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_g4_best_fit_single_note() {
|
|
// B1: the smallest single covering note, on 4 fresh wallets (each with its own HashMap order).
|
|
for round in 0..4 {
|
|
let (wallet, _) = g4_wallet(&[0, 0, 5000, 20000, 100000, 300000]);
|
|
let ext = g4_ext_address(&wallet);
|
|
let tx = g4_send(&wallet, vec![(ext.as_str(), 50000, None)]).unwrap();
|
|
assert_eq!(g4_spent_values(&wallet, &tx), vec![100000], "round {}", round);
|
|
assert_eq!(tx.vin.len(), 0);
|
|
g4_zero_notes_untouched(&wallet);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_g4_chat_send_spends_one_fee_note() {
|
|
// B2: a HushChat-style send (value 0 + memo to an own address): target = fee, so exactly the
|
|
// 10000 note, and none of the zero-value notes.
|
|
let (wallet, _) = g4_wallet(&[0, 0, 0, 10000, 100000, 100000]);
|
|
let own = encode_payment_address(wallet.config.hrp_sapling_address(),
|
|
&wallet.zkeys.read().unwrap()[0].zaddress);
|
|
let tx = g4_send(&wallet, vec![(own.as_str(), 0, Some("hello".to_string()))]).unwrap();
|
|
assert_eq!(g4_spent_values(&wallet, &tx), vec![10000]);
|
|
g4_zero_notes_untouched(&wallet);
|
|
}
|
|
|
|
#[test]
|
|
fn test_g4_fewest_notes_largest_first() {
|
|
// B3: no single note covers 75000 + fee; the fewest notes are 3, and the last pick is the
|
|
// smallest one that still covers (20000, not 7000).
|
|
let (wallet, _) = g4_wallet(&[40000, 30000, 20000, 7000]);
|
|
let ext = g4_ext_address(&wallet);
|
|
let tx = g4_send(&wallet, vec![(ext.as_str(), 75000, None)]).unwrap();
|
|
assert_eq!(g4_spent_values(&wallet, &tx), vec![20000, 30000, 40000]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_g4_too_many_inputs_refused_before_proving() {
|
|
// B4: 260 x 1000, send 250_000: needs 260 notes > 250. Refused with the max sendable amount,
|
|
// without the word the GUI retries on, before any proof, and nothing is marked spent.
|
|
let (wallet, _) = g4_wallet(&vec![1000u64; 260]);
|
|
let ext = g4_ext_address(&wallet);
|
|
let (ss, so) = get_sapling_params().unwrap();
|
|
let branch_id = u32::from_str_radix("76b809bb", 16).unwrap();
|
|
let called = std::sync::atomic::AtomicBool::new(false);
|
|
|
|
let t0 = std::time::Instant::now();
|
|
let r = wallet.send_to_address(branch_id, &ss, &so, false, vec![(ext.as_str(), 250_000, None)],
|
|
&default_fee(), |_| {
|
|
called.store(true, std::sync::atomic::Ordering::SeqCst);
|
|
Ok(' '.to_string())
|
|
});
|
|
let elapsed = t0.elapsed();
|
|
|
|
let e = r.err().expect("a send needing 260 notes must be refused");
|
|
assert!(e.starts_with("Too many inputs:"), "{}", e);
|
|
assert!(e.contains("0.00240000"), "{}", e);
|
|
assert!(e.contains("260") && e.contains("250"), "{}", e);
|
|
assert!(!e.contains("Insufficient"), "{}", e);
|
|
assert!(!called.load(std::sync::atomic::Ordering::SeqCst), "the broadcast closure was called");
|
|
assert!(elapsed.as_secs() < 20, "refusal took {:?}: proofs were built", elapsed);
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert!(txs.values().flat_map(|t| t.notes.iter()).all(|nd| nd.unconfirmed_spent.is_none()));
|
|
}
|
|
assert!(wallet.mempool_txs.read().unwrap().is_empty());
|
|
assert_eq!(wallet.verified_zbalance(None), 260_000);
|
|
|
|
// 250 notes fit: 240_000 is exactly the advertised maximum.
|
|
// (Not proved here: 250 Groth16 proofs take minutes; the pure tests cover the count.)
|
|
}
|
|
|
|
#[test]
|
|
fn test_g4_utxos_cover_target_still_spends_smallest_note() {
|
|
// B5 (integration O4): z [100] + UTXO 50000, send 30000. The notes cannot cover 40000, the
|
|
// UTXO alone can: the one smallest non-zero note is spent too, so the change is a note's change
|
|
// (is_change, to the note's address) and `list` shows no phantom receive.
|
|
let (wallet, tip) = g4_wallet(&[100]);
|
|
let txid_t = g4_add_utxo(&wallet, 50000);
|
|
let ext = g4_ext_address(&wallet);
|
|
let fee = default_fee();
|
|
|
|
let sent_tx = g4_send(&wallet, vec![(ext.as_str(), 30000, None)]).unwrap();
|
|
let sent_txid = sent_tx.txid();
|
|
assert_eq!(g4_spent_values(&wallet, &sent_tx), vec![100]);
|
|
assert_eq!(sent_tx.vin.len(), 1);
|
|
assert_eq!(sent_tx.shielded_outputs.len(), 2);
|
|
|
|
let mut cb3 = FakeCompactBlock::new(2, tip);
|
|
cb3.add_tx(&sent_tx);
|
|
wallet.scan_block(&cb3.as_bytes()).unwrap();
|
|
wallet.scan_full_tx(&sent_tx, 2, 0);
|
|
|
|
{
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid_t].utxos[0].spent, Some(sent_txid));
|
|
let note100 = txs.values().flat_map(|t| t.notes.iter()).find(|nd| nd.note.value == 100).unwrap();
|
|
assert_eq!(note100.spent, Some(sent_txid));
|
|
assert_eq!(txs[&sent_txid].notes.len(), 1);
|
|
let change = &txs[&sent_txid].notes[0];
|
|
assert_eq!(change.note.value, 50000 + 100 - 30000 - fee);
|
|
assert!(change.is_change, "the change must be recorded as change");
|
|
assert_eq!(change.account, 0);
|
|
}
|
|
assert_eq!(wallet.zbalance(None), 50000 + 100 - 30000 - fee);
|
|
|
|
// `list` (LightClient::do_list_transactions) shows the send once, as -(amount + fee), and no
|
|
// incoming entry for it. The client lives in a temp dir (never the real wallet directory).
|
|
let tmp = tempdir::TempDir::new("g4-list").unwrap();
|
|
let mut config = get_test_config();
|
|
config.data_dir = Some(tmp.path().to_str().unwrap().to_string());
|
|
let lc = crate::lightclient::LightClient::new(&config, 0).unwrap();
|
|
*lc.wallet.write().unwrap() = wallet;
|
|
let list = lc.do_list_transactions();
|
|
let sent_s = format!("{}", sent_txid);
|
|
let entries: Vec<&json::JsonValue> = list.members().filter(|e| e["txid"].as_str() == Some(sent_s.as_str())).collect();
|
|
assert_eq!(entries.len(), 1, "list entries for the send: {}", list.pretty(2));
|
|
assert_eq!(entries[0]["amount"].as_i64(), Some(-((30000 + fee) as i64)), "{}", list.pretty(2));
|
|
assert!(entries[0]["address"].is_null(), "the send shows as a receive: {}", list.pretty(2));
|
|
}
|
|
|
|
#[test]
|
|
fn test_g4_utxos_reduce_what_notes_cover() {
|
|
// B6: z [20000, 15000, 100] + UTXO 10000, send 30000: the notes alone (35100) cannot cover
|
|
// 40000, so they cover 40000 - 10000 with the fewest notes: {20000, 15000}; the 100 stays.
|
|
let (wallet, _) = g4_wallet(&[20000, 15000, 100]);
|
|
let txid_t = g4_add_utxo(&wallet, 10000);
|
|
let ext = g4_ext_address(&wallet);
|
|
let tx = g4_send(&wallet, vec![(ext.as_str(), 30000, None)]).unwrap();
|
|
assert_eq!(g4_spent_values(&wallet, &tx), vec![15000, 20000]);
|
|
assert_eq!(tx.vin.len(), 1);
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert_eq!(txs[&txid_t].utxos[0].unconfirmed_spent, Some(tx.txid()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_g4_insufficient_text_unchanged() {
|
|
// B7: the insufficient-funds text the GUI matches on is unchanged.
|
|
let (wallet, _) = g4_wallet(&[5000]);
|
|
let ext = g4_ext_address(&wallet);
|
|
let e = g4_send(&wallet, vec![(ext.as_str(), 50000, None)]).err().unwrap();
|
|
assert!(e.starts_with("Insufficient verified funds (have 5000, need "), "{}", e);
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert!(txs.values().flat_map(|t| t.notes.iter()).all(|nd| nd.unconfirmed_spent.is_none()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_g4_transaction_too_large_refused() {
|
|
// 200 shielded outputs (+ change) leave no room for a single spend in the size budget: the
|
|
// send is refused before proving with the "Transaction too large" text (no "Insufficient").
|
|
let (wallet, _) = g4_wallet(&[1_000_000]);
|
|
let ext = g4_ext_address(&wallet);
|
|
let tos: Vec<(&str, u64, Option<String>)> = (0..200).map(|_| (ext.as_str(), 1u64, None)).collect();
|
|
let e = g4_send(&wallet, tos).err().unwrap();
|
|
assert!(e.starts_with("Transaction too large:"), "{}", e);
|
|
assert!(!e.contains("Insufficient"), "{}", e);
|
|
let txs = wallet.txs.read().unwrap();
|
|
assert!(txs.values().flat_map(|t| t.notes.iter()).all(|nd| nd.unconfirmed_spent.is_none()));
|
|
}
|