819 lines
30 KiB
Rust
819 lines
30 KiB
Rust
//! Functions for creating transactions.
|
|
//!
|
|
use rusqlite::{named_params, Row};
|
|
use std::convert::TryInto;
|
|
|
|
use ff::PrimeField;
|
|
|
|
use zcash_primitives::{
|
|
consensus::BlockHeight,
|
|
merkle_tree::IncrementalWitness,
|
|
sapling::{Diversifier, Rseed},
|
|
transaction::components::Amount,
|
|
zip32::AccountId,
|
|
};
|
|
|
|
use zcash_client_backend::wallet::SpendableNote;
|
|
|
|
use crate::{error::SqliteClientError, WalletDb};
|
|
|
|
fn to_spendable_note(row: &Row) -> Result<SpendableNote, SqliteClientError> {
|
|
let diversifier = {
|
|
let d: Vec<_> = row.get(0)?;
|
|
if d.len() != 11 {
|
|
return Err(SqliteClientError::CorruptedData(
|
|
"Invalid diversifier length".to_string(),
|
|
));
|
|
}
|
|
let mut tmp = [0; 11];
|
|
tmp.copy_from_slice(&d);
|
|
Diversifier(tmp)
|
|
};
|
|
|
|
let note_value = Amount::from_i64(row.get(1)?).unwrap();
|
|
|
|
let rseed = {
|
|
let rcm_bytes: Vec<_> = row.get(2)?;
|
|
|
|
// We store rcm directly in the data DB, regardless of whether the note
|
|
// used a v1 or v2 note plaintext, so for the purposes of spending let's
|
|
// pretend this is a pre-ZIP 212 note.
|
|
let rcm = Option::from(jubjub::Fr::from_repr(
|
|
rcm_bytes[..]
|
|
.try_into()
|
|
.map_err(|_| SqliteClientError::InvalidNote)?,
|
|
))
|
|
.ok_or(SqliteClientError::InvalidNote)?;
|
|
Rseed::BeforeZip212(rcm)
|
|
};
|
|
|
|
let witness = {
|
|
let d: Vec<_> = row.get(3)?;
|
|
IncrementalWitness::read(&d[..])?
|
|
};
|
|
|
|
Ok(SpendableNote {
|
|
diversifier,
|
|
note_value,
|
|
rseed,
|
|
witness,
|
|
})
|
|
}
|
|
|
|
#[deprecated(
|
|
note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletRead::get_spendable_sapling_notes instead."
|
|
)]
|
|
pub fn get_spendable_sapling_notes<P>(
|
|
wdb: &WalletDb<P>,
|
|
account: AccountId,
|
|
anchor_height: BlockHeight,
|
|
) -> Result<Vec<SpendableNote>, SqliteClientError> {
|
|
let mut stmt_select_notes = wdb.conn.prepare(
|
|
"SELECT diversifier, value, rcm, witness
|
|
FROM received_notes
|
|
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
|
INNER JOIN sapling_witnesses ON sapling_witnesses.note = received_notes.id_note
|
|
WHERE account = :account
|
|
AND spent IS NULL
|
|
AND transactions.block <= :anchor_height
|
|
AND sapling_witnesses.block = :anchor_height",
|
|
)?;
|
|
|
|
// Select notes
|
|
let notes = stmt_select_notes.query_and_then_named::<_, SqliteClientError, _>(
|
|
named_params![
|
|
":account": &u32::from(account),
|
|
":anchor_height": &u32::from(anchor_height),
|
|
],
|
|
to_spendable_note,
|
|
)?;
|
|
|
|
notes.collect::<Result<_, _>>()
|
|
}
|
|
|
|
#[deprecated(
|
|
note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletRead::select_spendable_sapling_notes instead."
|
|
)]
|
|
pub fn select_spendable_sapling_notes<P>(
|
|
wdb: &WalletDb<P>,
|
|
account: AccountId,
|
|
target_value: Amount,
|
|
anchor_height: BlockHeight,
|
|
) -> Result<Vec<SpendableNote>, SqliteClientError> {
|
|
// The goal of this SQL statement is to select the oldest notes until the required
|
|
// value has been reached, and then fetch the witnesses at the desired height for the
|
|
// selected notes. This is achieved in several steps:
|
|
//
|
|
// 1) Use a window function to create a view of all notes, ordered from oldest to
|
|
// newest, with an additional column containing a running sum:
|
|
// - Unspent notes accumulate the values of all unspent notes in that note's
|
|
// account, up to itself.
|
|
// - Spent notes accumulate the values of all notes in the transaction they were
|
|
// spent in, up to itself.
|
|
//
|
|
// 2) Select all unspent notes in the desired account, along with their running sum.
|
|
//
|
|
// 3) Select all notes for which the running sum was less than the required value, as
|
|
// well as a single note for which the sum was greater than or equal to the
|
|
// required value, bringing the sum of all selected notes across the threshold.
|
|
//
|
|
// 4) Match the selected notes against the witnesses at the desired height.
|
|
let mut stmt_select_notes = wdb.conn.prepare(
|
|
"WITH selected AS (
|
|
WITH eligible AS (
|
|
SELECT id_note, diversifier, value, rcm,
|
|
SUM(value) OVER
|
|
(PARTITION BY account, spent ORDER BY id_note) AS so_far
|
|
FROM received_notes
|
|
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
|
WHERE account = :account AND spent IS NULL AND transactions.block <= :anchor_height
|
|
)
|
|
SELECT * FROM eligible WHERE so_far < :target_value
|
|
UNION
|
|
SELECT * FROM (SELECT * FROM eligible WHERE so_far >= :target_value LIMIT 1)
|
|
), witnesses AS (
|
|
SELECT note, witness FROM sapling_witnesses
|
|
WHERE block = :anchor_height
|
|
)
|
|
SELECT selected.diversifier, selected.value, selected.rcm, witnesses.witness
|
|
FROM selected
|
|
INNER JOIN witnesses ON selected.id_note = witnesses.note",
|
|
)?;
|
|
|
|
// Select notes
|
|
let notes = stmt_select_notes.query_and_then_named::<_, SqliteClientError, _>(
|
|
named_params![
|
|
":account": &u32::from(account),
|
|
":anchor_height": &u32::from(anchor_height),
|
|
":target_value": &i64::from(target_value),
|
|
],
|
|
to_spendable_note,
|
|
)?;
|
|
|
|
notes.collect::<Result<_, _>>()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(deprecated)]
|
|
mod tests {
|
|
use rusqlite::Connection;
|
|
use std::collections::HashMap;
|
|
use tempfile::NamedTempFile;
|
|
|
|
use zcash_proofs::prover::LocalTxProver;
|
|
|
|
use zcash_primitives::{
|
|
block::BlockHash,
|
|
consensus::{BlockHeight, BranchId, Parameters},
|
|
legacy::TransparentAddress,
|
|
sapling::{
|
|
keys::DiversifiableFullViewingKey, note_encryption::try_sapling_output_recovery,
|
|
prover::TxProver,
|
|
},
|
|
transaction::{components::Amount, Transaction},
|
|
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
|
};
|
|
|
|
#[cfg(feature = "transparent-inputs")]
|
|
use zcash_primitives::legacy::keys as transparent;
|
|
|
|
use zcash_client_backend::{
|
|
data_api::{chain::scan_cached_blocks, wallet::create_spend_to_address, WalletRead},
|
|
keys::{sapling, UnifiedFullViewingKey},
|
|
wallet::OvkPolicy,
|
|
};
|
|
|
|
use crate::{
|
|
chain::init::init_cache_database,
|
|
tests::{self, fake_compact_block, insert_into_cache, network, sapling_activation_height},
|
|
wallet::{
|
|
get_balance, get_balance_at,
|
|
init::{init_accounts_table, init_blocks_table, init_wallet_db},
|
|
},
|
|
AccountId, BlockDb, DataConnStmtCache, WalletDb,
|
|
};
|
|
|
|
fn test_prover() -> impl TxProver {
|
|
match LocalTxProver::with_default_location() {
|
|
Some(tx_prover) => tx_prover,
|
|
None => {
|
|
panic!("Cannot locate the Zcash parameters. Please run zcash-fetch-params or fetch-params.sh to download the parameters, and then re-run the tests.");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn create_to_address_fails_on_incorrect_extsk() {
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_wallet_db(&db_data).unwrap();
|
|
|
|
let acct0 = AccountId::from(0);
|
|
let acct1 = AccountId::from(1);
|
|
|
|
// Add two accounts to the wallet
|
|
let extsk0 = sapling::spending_key(&[0u8; 32], network().coin_type(), acct0);
|
|
let extsk1 = sapling::spending_key(&[1u8; 32], network().coin_type(), acct1);
|
|
let dfvk0 = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk0));
|
|
let dfvk1 = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk1));
|
|
|
|
#[cfg(feature = "transparent-inputs")]
|
|
let ufvks = {
|
|
let tsk0 =
|
|
transparent::AccountPrivKey::from_seed(&network(), &[0u8; 32], acct0).unwrap();
|
|
let tsk1 =
|
|
transparent::AccountPrivKey::from_seed(&network(), &[1u8; 32], acct1).unwrap();
|
|
HashMap::from([
|
|
(
|
|
acct0,
|
|
UnifiedFullViewingKey::new(Some(tsk0.to_account_pubkey()), Some(dfvk0), None)
|
|
.unwrap(),
|
|
),
|
|
(
|
|
acct1,
|
|
UnifiedFullViewingKey::new(Some(tsk1.to_account_pubkey()), Some(dfvk1), None)
|
|
.unwrap(),
|
|
),
|
|
])
|
|
};
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
|
let ufvks = HashMap::from([
|
|
(
|
|
acct0,
|
|
UnifiedFullViewingKey::new(Some(dfvk0), None).unwrap(),
|
|
),
|
|
(
|
|
acct1,
|
|
UnifiedFullViewingKey::new(Some(dfvk1), None).unwrap(),
|
|
),
|
|
]);
|
|
|
|
init_accounts_table(&db_data, &ufvks).unwrap();
|
|
let to = extsk0.default_address().1.into();
|
|
|
|
// Invalid extsk for the given account should cause an error
|
|
let mut db_write = db_data.get_update_ops().unwrap();
|
|
match create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk1,
|
|
&to,
|
|
Amount::from_u64(1).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
) {
|
|
Ok(_) => panic!("Should have failed"),
|
|
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"),
|
|
}
|
|
|
|
match create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(1),
|
|
&extsk0,
|
|
&to,
|
|
Amount::from_u64(1).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
) {
|
|
Ok(_) => panic!("Should have failed"),
|
|
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 1"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn create_to_address_fails_with_no_blocks() {
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_wallet_db(&db_data).unwrap();
|
|
|
|
// Add an account to the wallet
|
|
let account_id = AccountId::from(0);
|
|
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id);
|
|
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
|
|
|
|
#[cfg(feature = "transparent-inputs")]
|
|
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap();
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
|
let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap();
|
|
let ufvks = HashMap::from([(account_id, ufvk)]);
|
|
init_accounts_table(&db_data, &ufvks).unwrap();
|
|
let to = extsk.default_address().1.into();
|
|
|
|
// We cannot do anything if we aren't synchronised
|
|
let mut db_write = db_data.get_update_ops().unwrap();
|
|
match create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk,
|
|
&to,
|
|
Amount::from_u64(1).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
) {
|
|
Ok(_) => panic!("Should have failed"),
|
|
Err(e) => assert_eq!(e.to_string(), "Must scan blocks first"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn create_to_address_fails_on_insufficient_balance() {
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_wallet_db(&db_data).unwrap();
|
|
init_blocks_table(
|
|
&db_data,
|
|
BlockHeight::from(1u32),
|
|
BlockHash([1; 32]),
|
|
1,
|
|
&[],
|
|
)
|
|
.unwrap();
|
|
|
|
// Add an account to the wallet
|
|
let account_id = AccountId::from(0);
|
|
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id);
|
|
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
|
|
#[cfg(feature = "transparent-inputs")]
|
|
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap();
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
|
let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap();
|
|
let ufvks = HashMap::from([(account_id, ufvk)]);
|
|
init_accounts_table(&db_data, &ufvks).unwrap();
|
|
let to = extsk.default_address().1.into();
|
|
|
|
// Account balance should be zero
|
|
assert_eq!(
|
|
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
|
Amount::zero()
|
|
);
|
|
|
|
// We cannot spend anything
|
|
let mut db_write = db_data.get_update_ops().unwrap();
|
|
match create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk,
|
|
&to,
|
|
Amount::from_u64(1).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
) {
|
|
Ok(_) => panic!("Should have failed"),
|
|
Err(e) => assert_eq!(
|
|
e.to_string(),
|
|
"Insufficient balance (have 0, need 1001 including fee)"
|
|
),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn create_to_address_fails_on_unverified_notes() {
|
|
let cache_file = NamedTempFile::new().unwrap();
|
|
let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap());
|
|
init_cache_database(&db_cache).unwrap();
|
|
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_wallet_db(&db_data).unwrap();
|
|
|
|
// Add an account to the wallet
|
|
let account_id = AccountId::from(0);
|
|
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id);
|
|
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
|
|
#[cfg(feature = "transparent-inputs")]
|
|
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
|
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
|
|
let ufvks = HashMap::from([(account_id, ufvk)]);
|
|
init_accounts_table(&db_data, &ufvks).unwrap();
|
|
|
|
// Add funds to the wallet in a single note
|
|
let value = Amount::from_u64(50000).unwrap();
|
|
let (cb, _) = fake_compact_block(
|
|
sapling_activation_height(),
|
|
BlockHash([0; 32]),
|
|
&dfvk,
|
|
value,
|
|
);
|
|
insert_into_cache(&db_cache, &cb);
|
|
let mut db_write = db_data.get_update_ops().unwrap();
|
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
|
|
|
// Verified balance matches total balance
|
|
let (_, anchor_height) = (&db_data)
|
|
.get_target_and_anchor_heights(10)
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
|
assert_eq!(
|
|
get_balance_at(&db_data, AccountId::from(0), anchor_height).unwrap(),
|
|
value
|
|
);
|
|
|
|
// Add more funds to the wallet in a second note
|
|
let (cb, _) = fake_compact_block(sapling_activation_height() + 1, cb.hash(), &dfvk, value);
|
|
insert_into_cache(&db_cache, &cb);
|
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
|
|
|
// Verified balance does not include the second note
|
|
let (_, anchor_height2) = (&db_data)
|
|
.get_target_and_anchor_heights(10)
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(
|
|
get_balance(&db_data, AccountId::from(0)).unwrap(),
|
|
(value + value).unwrap()
|
|
);
|
|
assert_eq!(
|
|
get_balance_at(&db_data, AccountId::from(0), anchor_height2).unwrap(),
|
|
value
|
|
);
|
|
|
|
// Spend fails because there are insufficient verified notes
|
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
|
let to = extsk2.default_address().1.into();
|
|
match create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk,
|
|
&to,
|
|
Amount::from_u64(70000).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
) {
|
|
Ok(_) => panic!("Should have failed"),
|
|
Err(e) => assert_eq!(
|
|
e.to_string(),
|
|
"Insufficient balance (have 50000, need 71000 including fee)"
|
|
),
|
|
}
|
|
|
|
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second
|
|
// note is verified
|
|
for i in 2..10 {
|
|
let (cb, _) =
|
|
fake_compact_block(sapling_activation_height() + i, cb.hash(), &dfvk, value);
|
|
insert_into_cache(&db_cache, &cb);
|
|
}
|
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
|
|
|
// Second spend still fails
|
|
match create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk,
|
|
&to,
|
|
Amount::from_u64(70000).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
) {
|
|
Ok(_) => panic!("Should have failed"),
|
|
Err(e) => assert_eq!(
|
|
e.to_string(),
|
|
"Insufficient balance (have 50000, need 71000 including fee)"
|
|
),
|
|
}
|
|
|
|
// Mine block 11 so that the second note becomes verified
|
|
let (cb, _) = fake_compact_block(sapling_activation_height() + 10, cb.hash(), &dfvk, value);
|
|
insert_into_cache(&db_cache, &cb);
|
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
|
|
|
// Second spend should now succeed
|
|
create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk,
|
|
&to,
|
|
Amount::from_u64(70000).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn create_to_address_fails_on_locked_notes() {
|
|
let cache_file = NamedTempFile::new().unwrap();
|
|
let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap());
|
|
init_cache_database(&db_cache).unwrap();
|
|
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_wallet_db(&db_data).unwrap();
|
|
|
|
// Add an account to the wallet
|
|
let account_id = AccountId::from(0);
|
|
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id);
|
|
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
|
|
#[cfg(feature = "transparent-inputs")]
|
|
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
|
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
|
|
let ufvks = HashMap::from([(account_id, ufvk)]);
|
|
init_accounts_table(&db_data, &ufvks).unwrap();
|
|
|
|
// Add funds to the wallet in a single note
|
|
let value = Amount::from_u64(50000).unwrap();
|
|
let (cb, _) = fake_compact_block(
|
|
sapling_activation_height(),
|
|
BlockHash([0; 32]),
|
|
&dfvk,
|
|
value,
|
|
);
|
|
insert_into_cache(&db_cache, &cb);
|
|
let mut db_write = db_data.get_update_ops().unwrap();
|
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
|
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
|
|
|
// Send some of the funds to another address
|
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
|
let to = extsk2.default_address().1.into();
|
|
create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk,
|
|
&to,
|
|
Amount::from_u64(15000).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
)
|
|
.unwrap();
|
|
|
|
// A second spend fails because there are no usable notes
|
|
match create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk,
|
|
&to,
|
|
Amount::from_u64(2000).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
) {
|
|
Ok(_) => panic!("Should have failed"),
|
|
Err(e) => assert_eq!(
|
|
e.to_string(),
|
|
"Insufficient balance (have 0, need 3000 including fee)"
|
|
),
|
|
}
|
|
|
|
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 21 (that don't send us funds)
|
|
// until just before the first transaction expires
|
|
for i in 1..22 {
|
|
let (cb, _) = fake_compact_block(
|
|
sapling_activation_height() + i,
|
|
cb.hash(),
|
|
&ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])).into(),
|
|
value,
|
|
);
|
|
insert_into_cache(&db_cache, &cb);
|
|
}
|
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
|
|
|
// Second spend still fails
|
|
match create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk,
|
|
&to,
|
|
Amount::from_u64(2000).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
) {
|
|
Ok(_) => panic!("Should have failed"),
|
|
Err(e) => assert_eq!(
|
|
e.to_string(),
|
|
"Insufficient balance (have 0, need 3000 including fee)"
|
|
),
|
|
}
|
|
|
|
// Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires
|
|
let (cb, _) = fake_compact_block(
|
|
sapling_activation_height() + 22,
|
|
cb.hash(),
|
|
&ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])).into(),
|
|
value,
|
|
);
|
|
insert_into_cache(&db_cache, &cb);
|
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
|
|
|
// Second spend should now succeed
|
|
create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk,
|
|
&to,
|
|
Amount::from_u64(2000).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn ovk_policy_prevents_recovery_from_chain() {
|
|
let network = tests::network();
|
|
let cache_file = NamedTempFile::new().unwrap();
|
|
let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap());
|
|
init_cache_database(&db_cache).unwrap();
|
|
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let db_data = WalletDb::for_path(data_file.path(), network).unwrap();
|
|
init_wallet_db(&db_data).unwrap();
|
|
|
|
// Add an account to the wallet
|
|
let account_id = AccountId::from(0);
|
|
let extsk = sapling::spending_key(&[0u8; 32], network.coin_type(), account_id);
|
|
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
|
|
#[cfg(feature = "transparent-inputs")]
|
|
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
|
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
|
|
let ufvks = HashMap::from([(account_id, ufvk)]);
|
|
init_accounts_table(&db_data, &ufvks).unwrap();
|
|
|
|
// Add funds to the wallet in a single note
|
|
let value = Amount::from_u64(50000).unwrap();
|
|
let (cb, _) = fake_compact_block(
|
|
sapling_activation_height(),
|
|
BlockHash([0; 32]),
|
|
&dfvk,
|
|
value,
|
|
);
|
|
insert_into_cache(&db_cache, &cb);
|
|
let mut db_write = db_data.get_update_ops().unwrap();
|
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
|
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
|
|
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
|
let addr2 = extsk2.default_address().1;
|
|
let to = addr2.clone().into();
|
|
|
|
let send_and_recover_with_policy = |db_write: &mut DataConnStmtCache<'_, _>, ovk_policy| {
|
|
let tx_row = create_spend_to_address(
|
|
db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk,
|
|
&to,
|
|
Amount::from_u64(15000).unwrap(),
|
|
None,
|
|
ovk_policy,
|
|
10,
|
|
)
|
|
.unwrap();
|
|
|
|
// Fetch the transaction from the database
|
|
let raw_tx: Vec<_> = db_write
|
|
.wallet_db
|
|
.conn
|
|
.query_row(
|
|
"SELECT raw FROM transactions
|
|
WHERE id_tx = ?",
|
|
&[tx_row],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap();
|
|
let tx = Transaction::read(&raw_tx[..], BranchId::Canopy).unwrap();
|
|
|
|
// Fetch the output index from the database
|
|
let output_index: i64 = db_write
|
|
.wallet_db
|
|
.conn
|
|
.query_row(
|
|
"SELECT output_index FROM sent_notes
|
|
WHERE tx = ?",
|
|
&[tx_row],
|
|
|row| row.get(0),
|
|
)
|
|
.unwrap();
|
|
|
|
let output = &tx.sapling_bundle().unwrap().shielded_outputs[output_index as usize];
|
|
|
|
try_sapling_output_recovery(
|
|
&network,
|
|
sapling_activation_height(),
|
|
&dfvk.fvk().ovk,
|
|
output,
|
|
)
|
|
};
|
|
|
|
// Send some of the funds to another address, keeping history.
|
|
// The recipient output is decryptable by the sender.
|
|
let (_, recovered_to, _) =
|
|
send_and_recover_with_policy(&mut db_write, OvkPolicy::Sender).unwrap();
|
|
assert_eq!(&recovered_to, &addr2);
|
|
|
|
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 22 (that don't send us funds)
|
|
// so that the first transaction expires
|
|
for i in 1..=22 {
|
|
let (cb, _) = fake_compact_block(
|
|
sapling_activation_height() + i,
|
|
cb.hash(),
|
|
&ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])).into(),
|
|
value,
|
|
);
|
|
insert_into_cache(&db_cache, &cb);
|
|
}
|
|
scan_cached_blocks(&network, &db_cache, &mut db_write, None).unwrap();
|
|
|
|
// Send the funds again, discarding history.
|
|
// Neither transaction output is decryptable by the sender.
|
|
assert!(send_and_recover_with_policy(&mut db_write, OvkPolicy::Discard).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn create_to_address_succeeds_to_t_addr_zero_change() {
|
|
let cache_file = NamedTempFile::new().unwrap();
|
|
let db_cache = BlockDb(Connection::open(cache_file.path()).unwrap());
|
|
init_cache_database(&db_cache).unwrap();
|
|
|
|
let data_file = NamedTempFile::new().unwrap();
|
|
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
|
|
init_wallet_db(&db_data).unwrap();
|
|
|
|
// Add an account to the wallet
|
|
let account_id = AccountId::from(0);
|
|
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id);
|
|
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk));
|
|
#[cfg(feature = "transparent-inputs")]
|
|
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
|
|
#[cfg(not(feature = "transparent-inputs"))]
|
|
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
|
|
let ufvks = HashMap::from([(account_id, ufvk)]);
|
|
init_accounts_table(&db_data, &ufvks).unwrap();
|
|
|
|
// Add funds to the wallet in a single note
|
|
let value = Amount::from_u64(51000).unwrap();
|
|
let (cb, _) = fake_compact_block(
|
|
sapling_activation_height(),
|
|
BlockHash([0; 32]),
|
|
&dfvk,
|
|
value,
|
|
);
|
|
insert_into_cache(&db_cache, &cb);
|
|
let mut db_write = db_data.get_update_ops().unwrap();
|
|
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
|
|
|
|
// Verified balance matches total balance
|
|
let (_, anchor_height) = (&db_data)
|
|
.get_target_and_anchor_heights(10)
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
|
|
assert_eq!(
|
|
get_balance_at(&db_data, AccountId::from(0), anchor_height).unwrap(),
|
|
value
|
|
);
|
|
|
|
let to = TransparentAddress::PublicKey([7; 20]).into();
|
|
create_spend_to_address(
|
|
&mut db_write,
|
|
&tests::network(),
|
|
test_prover(),
|
|
AccountId::from(0),
|
|
&extsk,
|
|
&to,
|
|
Amount::from_u64(50000).unwrap(),
|
|
None,
|
|
OvkPolicy::Sender,
|
|
10,
|
|
)
|
|
.unwrap();
|
|
}
|
|
}
|