2019-03-08 19:20:32 -08:00
|
|
|
//! Functions for creating transactions.
|
2020-08-26 14:47:47 -07:00
|
|
|
//!
|
|
|
|
use std::convert::TryInto;
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
use ff::PrimeField;
|
2020-08-04 14:52:56 -07:00
|
|
|
|
2019-03-08 19:20:32 -08:00
|
|
|
use zcash_primitives::{
|
2020-08-26 14:47:47 -07:00
|
|
|
consensus::{BlockHeight},
|
|
|
|
merkle_tree::IncrementalWitness,
|
|
|
|
primitives::{Diversifier, Rseed},
|
|
|
|
transaction::components::Amount,
|
2019-03-08 19:20:32 -08:00
|
|
|
};
|
|
|
|
|
2020-08-04 14:52:56 -07:00
|
|
|
use zcash_client_backend::{
|
2020-08-26 14:47:47 -07:00
|
|
|
data_api::{error::Error},
|
|
|
|
wallet::{AccountId, SpendableNote},
|
2020-08-04 14:52:56 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
use crate::{error::SqliteClientError, DataConnection};
|
2019-03-08 19:20:32 -08:00
|
|
|
|
2020-08-26 14:47:47 -07:00
|
|
|
pub fn select_spendable_notes(
|
2020-08-05 18:14:45 -07:00
|
|
|
data: &DataConnection,
|
2020-08-26 14:47:47 -07:00
|
|
|
account: AccountId,
|
|
|
|
target_value: Amount,
|
|
|
|
anchor_height: BlockHeight,
|
|
|
|
) -> Result<Vec<SpendableNote>, SqliteClientError> {
|
2019-03-08 19:20:32 -08:00
|
|
|
// 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.
|
2020-08-05 18:14:45 -07:00
|
|
|
let mut stmt_select_notes = data.0.prepare(
|
2019-03-08 19:20:32 -08:00
|
|
|
"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
|
2020-08-26 14:47:47 -07:00
|
|
|
WHERE account = :account AND spent IS NULL AND transactions.block <= :anchor_height
|
2019-03-08 19:20:32 -08:00
|
|
|
)
|
2020-08-26 14:47:47 -07:00
|
|
|
SELECT * FROM eligible WHERE so_far < :target_value
|
2019-03-08 19:20:32 -08:00
|
|
|
UNION
|
2020-08-26 14:47:47 -07:00
|
|
|
SELECT * FROM (SELECT * FROM eligible WHERE so_far >= :target_value LIMIT 1)
|
2019-03-08 19:20:32 -08:00
|
|
|
), witnesses AS (
|
|
|
|
SELECT note, witness FROM sapling_witnesses
|
2020-08-26 14:47:47 -07:00
|
|
|
WHERE block = :anchor_height
|
2019-03-08 19:20:32 -08:00
|
|
|
)
|
|
|
|
SELECT selected.diversifier, selected.value, selected.rcm, witnesses.witness
|
|
|
|
FROM selected
|
|
|
|
INNER JOIN witnesses ON selected.id_note = witnesses.note",
|
|
|
|
)?;
|
|
|
|
|
|
|
|
// Select notes
|
2020-08-26 14:47:47 -07:00
|
|
|
let notes = stmt_select_notes.query_and_then_named::<_, SqliteClientError, _>(
|
2019-03-08 19:20:32 -08:00
|
|
|
&[
|
2020-08-26 14:47:47 -07:00
|
|
|
(&"account", &i64::from(account.0)),
|
|
|
|
(&"anchor_height", &u32::from(anchor_height)),
|
|
|
|
(&"target_value", &i64::from(target_value)),
|
2019-03-08 19:20:32 -08:00
|
|
|
],
|
|
|
|
|row| {
|
|
|
|
let diversifier = {
|
|
|
|
let d: Vec<_> = row.get(0)?;
|
|
|
|
if d.len() != 11 {
|
2020-08-05 18:14:45 -07:00
|
|
|
return Err(SqliteClientError(Error::CorruptedData(
|
2019-03-08 19:20:32 -08:00
|
|
|
"Invalid diversifier length",
|
|
|
|
)));
|
|
|
|
}
|
|
|
|
let mut tmp = [0; 11];
|
|
|
|
tmp.copy_from_slice(&d);
|
|
|
|
Diversifier(tmp)
|
|
|
|
};
|
|
|
|
|
2020-08-26 14:47:47 -07:00
|
|
|
let note_value = Amount::from_i64(row.get(1)?).unwrap();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
2020-08-05 21:00:49 -07:00
|
|
|
let rseed = {
|
2020-10-24 04:05:15 -07:00
|
|
|
let rcm_bytes: Vec<_> = row.get(2)?;
|
2020-08-04 23:26:57 -07:00
|
|
|
|
zcash_client_sqlite: Read rcm correctly from data DB after Canopy
ZIP 212 alters the note plaintext to store a seed from which rcm is
derived, rather than storing rcm directly. In the mobile SDKs we only
need rcm, so for post-ZIP 212 notes, we derive rcm from the seed and
store rcm in the data DB.
However, when selecting notes to spend, `create_to_address` was using the
transaction's target height to determine if Canopy is active, and parsing
the rcm value as the seed if so. This effectively applied a seed->rcm
derivation to all selected notes' rcms once Canopy activated on the
chain. As a result, the note commitments were incorrect, and thus the
anchors derived from the witness paths were also incorrect. This caused
two kinds of observed failures:
- If more than one note was selected, the builder would fail with
"anchor mismatch", as the note commitments would be effectively
randomised, causing the derived anchors to also randomise.
- If a single note was selected, the transaction would be built using
the randomised anchor, and then rejected when sent to the network.
The fix is to "pretend" in `create_to_address` that all notes are
pre-ZIP 212 notes. This works fine because we never need to serialize
back to the note plaintext while spending a note.
2020-10-23 14:21:59 -07:00
|
|
|
// 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.
|
2020-10-24 04:05:15 -07:00
|
|
|
let rcm = jubjub::Fr::from_repr(
|
|
|
|
rcm_bytes[..]
|
zcash_client_sqlite: Read rcm correctly from data DB after Canopy
ZIP 212 alters the note plaintext to store a seed from which rcm is
derived, rather than storing rcm directly. In the mobile SDKs we only
need rcm, so for post-ZIP 212 notes, we derive rcm from the seed and
store rcm in the data DB.
However, when selecting notes to spend, `create_to_address` was using the
transaction's target height to determine if Canopy is active, and parsing
the rcm value as the seed if so. This effectively applied a seed->rcm
derivation to all selected notes' rcms once Canopy activated on the
chain. As a result, the note commitments were incorrect, and thus the
anchors derived from the witness paths were also incorrect. This caused
two kinds of observed failures:
- If more than one note was selected, the builder would fail with
"anchor mismatch", as the note commitments would be effectively
randomised, causing the derived anchors to also randomise.
- If a single note was selected, the transaction would be built using
the randomised anchor, and then rejected when sent to the network.
The fix is to "pretend" in `create_to_address` that all notes are
pre-ZIP 212 notes. This works fine because we never need to serialize
back to the note plaintext while spending a note.
2020-10-23 14:21:59 -07:00
|
|
|
.try_into()
|
2020-08-05 18:14:45 -07:00
|
|
|
.map_err(|_| SqliteClientError(Error::InvalidNote))?,
|
zcash_client_sqlite: Read rcm correctly from data DB after Canopy
ZIP 212 alters the note plaintext to store a seed from which rcm is
derived, rather than storing rcm directly. In the mobile SDKs we only
need rcm, so for post-ZIP 212 notes, we derive rcm from the seed and
store rcm in the data DB.
However, when selecting notes to spend, `create_to_address` was using the
transaction's target height to determine if Canopy is active, and parsing
the rcm value as the seed if so. This effectively applied a seed->rcm
derivation to all selected notes' rcms once Canopy activated on the
chain. As a result, the note commitments were incorrect, and thus the
anchors derived from the witness paths were also incorrect. This caused
two kinds of observed failures:
- If more than one note was selected, the builder would fail with
"anchor mismatch", as the note commitments would be effectively
randomised, causing the derived anchors to also randomise.
- If a single note was selected, the transaction would be built using
the randomised anchor, and then rejected when sent to the network.
The fix is to "pretend" in `create_to_address` that all notes are
pre-ZIP 212 notes. This works fine because we never need to serialize
back to the note plaintext while spending a note.
2020-10-23 14:21:59 -07:00
|
|
|
)
|
2020-08-05 18:14:45 -07:00
|
|
|
.ok_or(SqliteClientError(Error::InvalidNote))?;
|
2020-10-24 04:05:15 -07:00
|
|
|
Rseed::BeforeZip212(rcm)
|
2019-03-08 19:20:32 -08:00
|
|
|
};
|
|
|
|
|
2020-08-26 14:47:47 -07:00
|
|
|
let witness = {
|
2019-03-08 19:20:32 -08:00
|
|
|
let d: Vec<_> = row.get(3)?;
|
|
|
|
IncrementalWitness::read(&d[..])?
|
|
|
|
};
|
|
|
|
|
2020-08-26 14:47:47 -07:00
|
|
|
Ok(SpendableNote {
|
2019-03-08 19:20:32 -08:00
|
|
|
diversifier,
|
2020-08-26 14:47:47 -07:00
|
|
|
note_value,
|
|
|
|
rseed,
|
|
|
|
witness,
|
2019-03-08 19:20:32 -08:00
|
|
|
})
|
|
|
|
},
|
|
|
|
)?;
|
|
|
|
|
2020-08-26 14:47:47 -07:00
|
|
|
let notes: Vec<SpendableNote> = notes.collect::<Result<_, _>>()?;
|
|
|
|
Ok(notes)
|
2019-03-08 19:20:32 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2020-07-09 04:48:09 -07:00
|
|
|
use rusqlite::Connection;
|
2019-03-08 19:20:32 -08:00
|
|
|
use tempfile::NamedTempFile;
|
2020-08-05 13:27:40 -07:00
|
|
|
|
2019-03-08 19:20:32 -08:00
|
|
|
use zcash_primitives::{
|
|
|
|
block::BlockHash,
|
2020-08-26 14:47:47 -07:00
|
|
|
consensus::BlockHeight,
|
2020-07-09 04:48:09 -07:00
|
|
|
note_encryption::try_sapling_output_recovery,
|
2019-03-08 19:20:32 -08:00
|
|
|
prover::TxProver,
|
2020-07-09 04:48:09 -07:00
|
|
|
transaction::{components::Amount, Transaction},
|
2019-03-08 19:20:32 -08:00
|
|
|
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
|
|
|
};
|
2020-08-05 13:27:40 -07:00
|
|
|
|
2019-03-08 19:20:32 -08:00
|
|
|
use zcash_proofs::prover::LocalTxProver;
|
|
|
|
|
2020-08-26 14:47:47 -07:00
|
|
|
use zcash_client_backend::{
|
|
|
|
data_api::{chain::scan_cached_blocks, wallet::create_spend_to_address, DBOps},
|
|
|
|
wallet::OvkPolicy,
|
|
|
|
};
|
2020-08-20 16:03:43 -07:00
|
|
|
|
2019-03-08 19:20:32 -08:00
|
|
|
use crate::{
|
2020-08-25 14:29:01 -07:00
|
|
|
chain::init::init_cache_database,
|
2020-08-05 13:27:40 -07:00
|
|
|
tests::{self, fake_compact_block, insert_into_cache, sapling_activation_height},
|
2020-08-25 14:29:01 -07:00
|
|
|
wallet::{
|
|
|
|
get_balance, get_verified_balance,
|
|
|
|
init::{init_accounts_table, init_blocks_table, init_data_database},
|
|
|
|
},
|
2020-08-06 13:11:25 -07:00
|
|
|
AccountId, CacheConnection, DataConnection,
|
2019-03-08 19:20:32 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
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();
|
2020-08-05 18:14:45 -07:00
|
|
|
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
2019-03-08 19:20:32 -08:00
|
|
|
init_data_database(&db_data).unwrap();
|
|
|
|
|
|
|
|
// Add two accounts to the wallet
|
|
|
|
let extsk0 = ExtendedSpendingKey::master(&[]);
|
|
|
|
let extsk1 = ExtendedSpendingKey::master(&[0]);
|
|
|
|
let extfvks = [
|
|
|
|
ExtendedFullViewingKey::from(&extsk0),
|
|
|
|
ExtendedFullViewingKey::from(&extsk1),
|
|
|
|
];
|
2020-08-05 13:27:40 -07:00
|
|
|
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap();
|
2019-05-24 07:59:18 -07:00
|
|
|
let to = extsk0.default_address().unwrap().1.into();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Invalid extsk for the given account should cause an error
|
2020-08-26 14:47:47 -07:00
|
|
|
match create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&tests::network(),
|
2019-03-08 19:20:32 -08:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(0),
|
|
|
|
&extsk1,
|
2019-03-08 19:20:32 -08:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(1).unwrap(),
|
|
|
|
None,
|
2020-07-09 04:48:09 -07:00
|
|
|
OvkPolicy::Sender,
|
2019-03-08 19:20:32 -08:00
|
|
|
) {
|
|
|
|
Ok(_) => panic!("Should have failed"),
|
|
|
|
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"),
|
|
|
|
}
|
2020-08-05 18:14:45 -07:00
|
|
|
|
2020-08-26 14:47:47 -07:00
|
|
|
match create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&tests::network(),
|
2019-03-08 19:20:32 -08:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(1),
|
|
|
|
&extsk0,
|
2019-03-08 19:20:32 -08:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(1).unwrap(),
|
|
|
|
None,
|
2020-07-09 04:48:09 -07:00
|
|
|
OvkPolicy::Sender,
|
2019-03-08 19:20:32 -08:00
|
|
|
) {
|
|
|
|
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();
|
2020-08-05 18:14:45 -07:00
|
|
|
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
2019-03-08 19:20:32 -08:00
|
|
|
init_data_database(&db_data).unwrap();
|
|
|
|
|
|
|
|
// Add an account to the wallet
|
|
|
|
let extsk = ExtendedSpendingKey::master(&[]);
|
|
|
|
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
|
2020-08-05 13:27:40 -07:00
|
|
|
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap();
|
2019-05-24 07:59:18 -07:00
|
|
|
let to = extsk.default_address().unwrap().1.into();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// We cannot do anything if we aren't synchronised
|
2020-08-26 14:47:47 -07:00
|
|
|
match create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&tests::network(),
|
2019-03-08 19:20:32 -08:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(0),
|
|
|
|
&extsk,
|
2019-03-08 19:20:32 -08:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(1).unwrap(),
|
|
|
|
None,
|
2020-07-09 04:48:09 -07:00
|
|
|
OvkPolicy::Sender,
|
2019-03-08 19:20:32 -08:00
|
|
|
) {
|
|
|
|
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();
|
2020-08-05 18:14:45 -07:00
|
|
|
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
2019-03-08 19:20:32 -08:00
|
|
|
init_data_database(&db_data).unwrap();
|
2020-08-05 16:01:22 -07:00
|
|
|
init_blocks_table(
|
|
|
|
&db_data,
|
|
|
|
BlockHeight::from(1u32),
|
|
|
|
BlockHash([1; 32]),
|
|
|
|
1,
|
|
|
|
&[],
|
|
|
|
)
|
|
|
|
.unwrap();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Add an account to the wallet
|
|
|
|
let extsk = ExtendedSpendingKey::master(&[]);
|
|
|
|
let extfvks = [ExtendedFullViewingKey::from(&extsk)];
|
2020-08-05 13:27:40 -07:00
|
|
|
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap();
|
2019-05-24 07:59:18 -07:00
|
|
|
let to = extsk.default_address().unwrap().1.into();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Account balance should be zero
|
2020-08-06 13:11:25 -07:00
|
|
|
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero());
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// We cannot spend anything
|
2020-08-26 14:47:47 -07:00
|
|
|
match create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&tests::network(),
|
2019-03-08 19:20:32 -08:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(0),
|
|
|
|
&extsk,
|
2019-03-08 19:20:32 -08:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(1).unwrap(),
|
|
|
|
None,
|
2020-07-09 04:48:09 -07:00
|
|
|
OvkPolicy::Sender,
|
2019-03-08 19:20:32 -08:00
|
|
|
) {
|
|
|
|
Ok(_) => panic!("Should have failed"),
|
|
|
|
Err(e) => assert_eq!(
|
|
|
|
e.to_string(),
|
2020-11-23 17:50:47 -08:00
|
|
|
"Insufficient balance (have 0, need 1001 including fee)"
|
2019-03-08 19:20:32 -08:00
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn create_to_address_fails_on_unverified_notes() {
|
|
|
|
let cache_file = NamedTempFile::new().unwrap();
|
2020-08-05 18:14:45 -07:00
|
|
|
let db_cache = CacheConnection(Connection::open(cache_file.path()).unwrap());
|
2019-03-08 19:20:32 -08:00
|
|
|
init_cache_database(&db_cache).unwrap();
|
|
|
|
|
|
|
|
let data_file = NamedTempFile::new().unwrap();
|
2020-08-05 18:14:45 -07:00
|
|
|
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
2019-03-08 19:20:32 -08:00
|
|
|
init_data_database(&db_data).unwrap();
|
|
|
|
|
|
|
|
// Add an account to the wallet
|
|
|
|
let extsk = ExtendedSpendingKey::master(&[]);
|
|
|
|
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
2020-08-05 13:27:40 -07:00
|
|
|
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Add funds to the wallet in a single note
|
|
|
|
let value = Amount::from_u64(50000).unwrap();
|
|
|
|
let (cb, _) = fake_compact_block(
|
2020-08-05 13:27:40 -07:00
|
|
|
sapling_activation_height(),
|
2019-03-08 19:20:32 -08:00
|
|
|
BlockHash([0; 32]),
|
|
|
|
extfvk.clone(),
|
|
|
|
value,
|
|
|
|
);
|
2020-08-05 18:14:45 -07:00
|
|
|
insert_into_cache(&db_cache, &cb);
|
|
|
|
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Verified balance matches total balance
|
2020-08-26 14:47:47 -07:00
|
|
|
let (_, anchor_height) = (&db_data).get_target_and_anchor_heights().unwrap().unwrap();
|
2020-08-06 13:11:25 -07:00
|
|
|
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value);
|
2020-08-26 14:47:47 -07:00
|
|
|
assert_eq!(
|
|
|
|
get_verified_balance(&db_data, AccountId(0), anchor_height).unwrap(),
|
|
|
|
value
|
|
|
|
);
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Add more funds to the wallet in a second note
|
|
|
|
let (cb, _) = fake_compact_block(
|
2020-08-05 13:27:40 -07:00
|
|
|
sapling_activation_height() + 1,
|
2019-03-08 19:20:32 -08:00
|
|
|
cb.hash(),
|
|
|
|
extfvk.clone(),
|
|
|
|
value,
|
|
|
|
);
|
2020-08-05 18:14:45 -07:00
|
|
|
insert_into_cache(&db_cache, &cb);
|
|
|
|
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Verified balance does not include the second note
|
2020-08-26 14:47:47 -07:00
|
|
|
let (_, anchor_height2) = (&db_data).get_target_and_anchor_heights().unwrap().unwrap();
|
2020-08-06 13:11:25 -07:00
|
|
|
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value + value);
|
2020-08-26 14:47:47 -07:00
|
|
|
assert_eq!(
|
|
|
|
get_verified_balance(&db_data, AccountId(0), anchor_height2).unwrap(),
|
|
|
|
value
|
|
|
|
);
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Spend fails because there are insufficient verified notes
|
|
|
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
2019-05-24 07:59:18 -07:00
|
|
|
let to = extsk2.default_address().unwrap().1.into();
|
2020-08-26 14:47:47 -07:00
|
|
|
match create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&tests::network(),
|
2019-03-08 19:20:32 -08:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(0),
|
|
|
|
&extsk,
|
2019-03-08 19:20:32 -08:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(70000).unwrap(),
|
|
|
|
None,
|
2020-07-09 04:48:09 -07:00
|
|
|
OvkPolicy::Sender,
|
2019-03-08 19:20:32 -08:00
|
|
|
) {
|
|
|
|
Ok(_) => panic!("Should have failed"),
|
|
|
|
Err(e) => assert_eq!(
|
|
|
|
e.to_string(),
|
2020-11-23 17:50:47 -08:00
|
|
|
"Insufficient balance (have 50000, need 71000 including fee)"
|
2019-03-08 19:20:32 -08:00
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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(
|
2020-08-05 13:27:40 -07:00
|
|
|
sapling_activation_height() + i,
|
2019-03-08 19:20:32 -08:00
|
|
|
cb.hash(),
|
|
|
|
extfvk.clone(),
|
|
|
|
value,
|
|
|
|
);
|
2020-08-05 18:14:45 -07:00
|
|
|
insert_into_cache(&db_cache, &cb);
|
2019-03-08 19:20:32 -08:00
|
|
|
}
|
2020-08-05 18:14:45 -07:00
|
|
|
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Second spend still fails
|
2020-08-26 14:47:47 -07:00
|
|
|
match create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&tests::network(),
|
2019-03-08 19:20:32 -08:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(0),
|
|
|
|
&extsk,
|
2019-03-08 19:20:32 -08:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(70000).unwrap(),
|
|
|
|
None,
|
2020-07-09 04:48:09 -07:00
|
|
|
OvkPolicy::Sender,
|
2019-03-08 19:20:32 -08:00
|
|
|
) {
|
|
|
|
Ok(_) => panic!("Should have failed"),
|
|
|
|
Err(e) => assert_eq!(
|
|
|
|
e.to_string(),
|
2020-11-23 17:50:47 -08:00
|
|
|
"Insufficient balance (have 50000, need 71000 including fee)"
|
2019-03-08 19:20:32 -08:00
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mine block 11 so that the second note becomes verified
|
|
|
|
let (cb, _) = fake_compact_block(
|
2020-08-05 13:27:40 -07:00
|
|
|
sapling_activation_height() + 10,
|
2019-03-08 19:20:32 -08:00
|
|
|
cb.hash(),
|
|
|
|
extfvk.clone(),
|
|
|
|
value,
|
|
|
|
);
|
2020-08-05 18:14:45 -07:00
|
|
|
insert_into_cache(&db_cache, &cb);
|
|
|
|
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Second spend should now succeed
|
2020-08-26 14:47:47 -07:00
|
|
|
create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&tests::network(),
|
2019-03-08 19:20:32 -08:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(0),
|
|
|
|
&extsk,
|
2019-03-08 19:20:32 -08:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(70000).unwrap(),
|
|
|
|
None,
|
2020-07-09 04:48:09 -07:00
|
|
|
OvkPolicy::Sender,
|
2019-03-08 19:20:32 -08:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn create_to_address_fails_on_locked_notes() {
|
|
|
|
let cache_file = NamedTempFile::new().unwrap();
|
2020-08-05 18:14:45 -07:00
|
|
|
let db_cache = CacheConnection(Connection::open(cache_file.path()).unwrap());
|
2019-03-08 19:20:32 -08:00
|
|
|
init_cache_database(&db_cache).unwrap();
|
|
|
|
|
|
|
|
let data_file = NamedTempFile::new().unwrap();
|
2020-08-05 18:14:45 -07:00
|
|
|
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
2019-03-08 19:20:32 -08:00
|
|
|
init_data_database(&db_data).unwrap();
|
|
|
|
|
|
|
|
// Add an account to the wallet
|
|
|
|
let extsk = ExtendedSpendingKey::master(&[]);
|
|
|
|
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
2020-08-05 13:27:40 -07:00
|
|
|
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Add funds to the wallet in a single note
|
|
|
|
let value = Amount::from_u64(50000).unwrap();
|
|
|
|
let (cb, _) = fake_compact_block(
|
2020-08-05 13:27:40 -07:00
|
|
|
sapling_activation_height(),
|
2019-03-08 19:20:32 -08:00
|
|
|
BlockHash([0; 32]),
|
|
|
|
extfvk.clone(),
|
|
|
|
value,
|
|
|
|
);
|
2020-08-05 18:14:45 -07:00
|
|
|
insert_into_cache(&db_cache, &cb);
|
|
|
|
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
2020-08-06 13:11:25 -07:00
|
|
|
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value);
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Send some of the funds to another address
|
|
|
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
2019-05-24 07:59:18 -07:00
|
|
|
let to = extsk2.default_address().unwrap().1.into();
|
2020-08-26 14:47:47 -07:00
|
|
|
create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&tests::network(),
|
2019-03-08 19:20:32 -08:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(0),
|
|
|
|
&extsk,
|
2019-03-08 19:20:32 -08:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(15000).unwrap(),
|
|
|
|
None,
|
2020-07-09 04:48:09 -07:00
|
|
|
OvkPolicy::Sender,
|
2019-03-08 19:20:32 -08:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
// A second spend fails because there are no usable notes
|
2020-08-26 14:47:47 -07:00
|
|
|
match create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&tests::network(),
|
2019-03-08 19:20:32 -08:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(0),
|
|
|
|
&extsk,
|
2019-03-08 19:20:32 -08:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(2000).unwrap(),
|
|
|
|
None,
|
2020-07-09 04:48:09 -07:00
|
|
|
OvkPolicy::Sender,
|
2019-03-08 19:20:32 -08:00
|
|
|
) {
|
|
|
|
Ok(_) => panic!("Should have failed"),
|
|
|
|
Err(e) => assert_eq!(
|
|
|
|
e.to_string(),
|
2020-11-23 17:50:47 -08:00
|
|
|
"Insufficient balance (have 0, need 3000 including fee)"
|
2019-03-08 19:20:32 -08:00
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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(
|
2020-08-05 13:27:40 -07:00
|
|
|
sapling_activation_height() + i,
|
2019-03-08 19:20:32 -08:00
|
|
|
cb.hash(),
|
|
|
|
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])),
|
|
|
|
value,
|
|
|
|
);
|
2020-08-05 18:14:45 -07:00
|
|
|
insert_into_cache(&db_cache, &cb);
|
2019-03-08 19:20:32 -08:00
|
|
|
}
|
2020-08-05 18:14:45 -07:00
|
|
|
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Second spend still fails
|
2020-08-26 14:47:47 -07:00
|
|
|
match create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&tests::network(),
|
2019-03-08 19:20:32 -08:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(0),
|
|
|
|
&extsk,
|
2019-03-08 19:20:32 -08:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(2000).unwrap(),
|
|
|
|
None,
|
2020-07-09 04:48:09 -07:00
|
|
|
OvkPolicy::Sender,
|
2019-03-08 19:20:32 -08:00
|
|
|
) {
|
|
|
|
Ok(_) => panic!("Should have failed"),
|
|
|
|
Err(e) => assert_eq!(
|
|
|
|
e.to_string(),
|
2020-11-23 17:50:47 -08:00
|
|
|
"Insufficient balance (have 0, need 3000 including fee)"
|
2019-03-08 19:20:32 -08:00
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires
|
|
|
|
let (cb, _) = fake_compact_block(
|
2020-08-05 13:27:40 -07:00
|
|
|
sapling_activation_height() + 22,
|
2019-03-08 19:20:32 -08:00
|
|
|
cb.hash(),
|
|
|
|
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])),
|
|
|
|
value,
|
|
|
|
);
|
2020-08-05 18:14:45 -07:00
|
|
|
insert_into_cache(&db_cache, &cb);
|
|
|
|
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
2019-03-08 19:20:32 -08:00
|
|
|
|
|
|
|
// Second spend should now succeed
|
2020-08-26 14:47:47 -07:00
|
|
|
create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&tests::network(),
|
2019-03-08 19:20:32 -08:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(0),
|
|
|
|
&extsk,
|
2019-03-08 19:20:32 -08:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(2000).unwrap(),
|
|
|
|
None,
|
2020-07-09 04:48:09 -07:00
|
|
|
OvkPolicy::Sender,
|
2019-03-08 19:20:32 -08:00
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
}
|
2020-07-09 04:48:09 -07:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn ovk_policy_prevents_recovery_from_chain() {
|
2020-08-05 13:27:40 -07:00
|
|
|
let network = tests::network();
|
2020-07-09 04:48:09 -07:00
|
|
|
let cache_file = NamedTempFile::new().unwrap();
|
2020-08-05 18:14:45 -07:00
|
|
|
let db_cache = CacheConnection(Connection::open(cache_file.path()).unwrap());
|
2020-07-09 04:48:09 -07:00
|
|
|
init_cache_database(&db_cache).unwrap();
|
|
|
|
|
|
|
|
let data_file = NamedTempFile::new().unwrap();
|
2020-08-05 18:14:45 -07:00
|
|
|
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
2020-07-09 04:48:09 -07:00
|
|
|
init_data_database(&db_data).unwrap();
|
|
|
|
|
|
|
|
// Add an account to the wallet
|
|
|
|
let extsk = ExtendedSpendingKey::master(&[]);
|
|
|
|
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
2020-08-05 13:27:40 -07:00
|
|
|
init_accounts_table(&db_data, &network, &[extfvk.clone()]).unwrap();
|
2020-07-09 04:48:09 -07:00
|
|
|
|
|
|
|
// Add funds to the wallet in a single note
|
|
|
|
let value = Amount::from_u64(50000).unwrap();
|
|
|
|
let (cb, _) = fake_compact_block(
|
2020-08-05 13:27:40 -07:00
|
|
|
sapling_activation_height(),
|
2020-07-09 04:48:09 -07:00
|
|
|
BlockHash([0; 32]),
|
|
|
|
extfvk.clone(),
|
|
|
|
value,
|
|
|
|
);
|
2020-08-05 18:14:45 -07:00
|
|
|
insert_into_cache(&db_cache, &cb);
|
2020-08-06 13:11:25 -07:00
|
|
|
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
|
|
|
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value);
|
2020-07-09 04:48:09 -07:00
|
|
|
|
|
|
|
let extsk2 = ExtendedSpendingKey::master(&[]);
|
|
|
|
let addr2 = extsk2.default_address().unwrap().1;
|
|
|
|
let to = addr2.clone().into();
|
|
|
|
|
|
|
|
let send_and_recover_with_policy = |ovk_policy| {
|
2020-08-26 14:47:47 -07:00
|
|
|
let tx_row = create_spend_to_address(
|
2020-08-05 18:14:45 -07:00
|
|
|
&db_data,
|
2020-08-05 13:27:40 -07:00
|
|
|
&network,
|
2020-07-09 04:48:09 -07:00
|
|
|
test_prover(),
|
2020-08-26 14:47:47 -07:00
|
|
|
AccountId(0),
|
|
|
|
&extsk,
|
2020-07-09 04:48:09 -07:00
|
|
|
&to,
|
|
|
|
Amount::from_u64(15000).unwrap(),
|
|
|
|
None,
|
|
|
|
ovk_policy,
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
// Fetch the transaction from the database
|
2020-08-05 18:14:45 -07:00
|
|
|
let raw_tx: Vec<_> = db_data
|
|
|
|
.0
|
2020-07-09 04:48:09 -07:00
|
|
|
.query_row(
|
|
|
|
"SELECT raw FROM transactions
|
|
|
|
WHERE id_tx = ?",
|
|
|
|
&[tx_row],
|
|
|
|
|row| row.get(0),
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
let tx = Transaction::read(&raw_tx[..]).unwrap();
|
|
|
|
|
|
|
|
// Fetch the output index from the database
|
2020-08-05 18:14:45 -07:00
|
|
|
let output_index: i64 = db_data
|
|
|
|
.0
|
2020-07-09 04:48:09 -07:00
|
|
|
.query_row(
|
|
|
|
"SELECT output_index FROM sent_notes
|
|
|
|
WHERE tx = ?",
|
|
|
|
&[tx_row],
|
|
|
|
|row| row.get(0),
|
|
|
|
)
|
|
|
|
.unwrap();
|
2020-08-25 17:31:21 -07:00
|
|
|
|
2020-07-09 04:48:09 -07:00
|
|
|
let output = &tx.shielded_outputs[output_index as usize];
|
|
|
|
|
2020-08-05 13:27:40 -07:00
|
|
|
try_sapling_output_recovery(
|
|
|
|
&network,
|
|
|
|
sapling_activation_height(),
|
2020-07-09 04:48:09 -07:00
|
|
|
&extfvk.fvk.ovk,
|
|
|
|
&output.cv,
|
|
|
|
&output.cmu,
|
2020-09-09 16:39:21 -07:00
|
|
|
&output.ephemeral_key,
|
2020-07-09 04:48:09 -07:00
|
|
|
&output.enc_ciphertext,
|
|
|
|
&output.out_ciphertext,
|
|
|
|
)
|
|
|
|
};
|
|
|
|
|
|
|
|
// 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(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(
|
2020-08-05 13:27:40 -07:00
|
|
|
sapling_activation_height() + i,
|
2020-07-09 04:48:09 -07:00
|
|
|
cb.hash(),
|
|
|
|
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])),
|
|
|
|
value,
|
|
|
|
);
|
2020-08-05 18:14:45 -07:00
|
|
|
insert_into_cache(&db_cache, &cb);
|
2020-07-09 04:48:09 -07:00
|
|
|
}
|
2020-08-05 18:14:45 -07:00
|
|
|
scan_cached_blocks(&network, &db_cache, &db_data, None).unwrap();
|
2020-07-09 04:48:09 -07:00
|
|
|
|
|
|
|
// Send the funds again, discarding history.
|
|
|
|
// Neither transaction output is decryptable by the sender.
|
|
|
|
assert!(send_and_recover_with_policy(OvkPolicy::Discard).is_none());
|
|
|
|
}
|
2019-03-08 19:20:32 -08:00
|
|
|
}
|