Move 'create_spend_to_address' to wallet backend.

This required changing a bit about the relationship between
database errors and wallet errors, and opens up the possibility
of now simplifying the error situation a bit.
This commit is contained in:
Kris Nuttycombe 2020-08-26 15:47:47 -06:00
parent e144015558
commit cd2729bbd0
10 changed files with 516 additions and 416 deletions

View File

@ -22,7 +22,9 @@ hex = "0.4"
jubjub = "0.5.1"
nom = "5.1.2"
protobuf = "2.15"
rand_core = "0.5.1"
subtle = "2.2.3"
time = "0.2"
zcash_primitives = { version = "0.4", path = "../zcash_primitives" }
proptest = { version = "0.10.1", optional = true }
percent-encoding = "2.1.0"
@ -35,6 +37,7 @@ rand_core = "0.5.1"
rand_xorshift = "0.2"
tempfile = "3.1.0"
zcash_client_sqlite = { version = "0.2", path = "../zcash_client_sqlite" }
zcash_proofs = { version = "0.4", path = "../zcash_proofs" }
[features]
test-dependencies = ["proptest", "zcash_primitives/test-dependencies"]

View File

@ -1,5 +1,3 @@
use std::cmp;
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight, NetworkUpgrade},
@ -16,8 +14,6 @@ use crate::{
welding_rig::scan_block,
};
pub const ANCHOR_OFFSET: u32 = 10;
/// Checks that the scanned blocks in the data database, when combined with the recent
/// `CompactBlock`s in the cache database, form a valid chain.
///
@ -91,34 +87,6 @@ where
}
}
/// Determines the target height for a transaction, and the height from which to
/// select anchors, based on the current synchronised block chain.
pub fn get_target_and_anchor_heights<'db, E0, N, E, D>(
data: &'db D,
) -> Result<(BlockHeight, BlockHeight), E>
where
E: From<Error<E0, N>>,
&'db D: DBOps<Error = E>,
{
data.block_height_extrema().and_then(|heights| {
match heights {
Some((min_height, max_height)) => {
let target_height = max_height + 1;
// Select an anchor ANCHOR_OFFSET back from the target block,
// unless that would be before the earliest block we have.
let anchor_height = BlockHeight::from(cmp::max(
u32::from(target_height).saturating_sub(ANCHOR_OFFSET),
u32::from(min_height),
));
Ok((target_height, anchor_height))
}
None => Err(Error::ScanRequired.into()),
}
})
}
/// Scans at most `limit` new blocks added to the cache for any transactions received by
/// the tracked accounts.
///

View File

@ -3,9 +3,11 @@ use std::fmt;
use zcash_primitives::{
consensus::BlockHeight,
sapling::Node,
transaction::{builder, TxId},
transaction::{builder, components::amount::Amount, TxId},
};
use crate::wallet::AccountId;
#[derive(Debug)]
pub enum ChainInvalid {
PrevHashMismatch,
@ -17,9 +19,9 @@ pub enum ChainInvalid {
pub enum Error<DbError, NoteId> {
CorruptedData(&'static str),
IncorrectHRPExtFVK,
InsufficientBalance(u64, u64),
InsufficientBalance(Amount, Amount),
InvalidChain(BlockHeight, ChainInvalid),
InvalidExtSK(u32),
InvalidExtSK(AccountId),
InvalidMemo(std::str::Utf8Error),
InvalidNewWitnessAnchor(usize, TxId, BlockHeight, Node),
InvalidNote,
@ -53,13 +55,13 @@ impl<E: fmt::Display, N: fmt::Display> fmt::Display for Error<E, N> {
Error::InsufficientBalance(have, need) => write!(
f,
"Insufficient balance (have {}, need {} including fee)",
have, need
i64::from(*have), i64::from(*need)
),
Error::InvalidChain(upper_bound, cause) => {
write!(f, "Invalid chain (upper bound: {}): {:?}", u32::from(*upper_bound), cause)
}
Error::InvalidExtSK(account) => {
write!(f, "Incorrect ExtendedSpendingKey for account {}", account)
write!(f, "Incorrect ExtendedSpendingKey for account {}", account.0)
}
Error::InvalidMemo(e) => write!(f, "{}", e),
Error::InvalidNewWitnessAnchor(output, txid, last_height, anchor) => write!(

View File

@ -1,3 +1,5 @@
use std::cmp;
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
@ -11,9 +13,10 @@ use zcash_primitives::{
use crate::{
address::RecipientAddress,
data_api::wallet::ANCHOR_OFFSET,
decrypt::DecryptedOutput,
proto::compact_formats::CompactBlock,
wallet::{AccountId, WalletShieldedOutput, WalletTx},
wallet::{AccountId, SpendableNote, WalletShieldedOutput, WalletTx},
};
pub mod chain;
@ -23,7 +26,8 @@ pub mod wallet;
pub trait DBOps {
type Error;
type NoteRef: Copy; // Backend-specific note identifier
type UpdateOps: DBUpdate<Error = Self::Error, NoteRef = Self::NoteRef>;
type TxRef: Copy;
type UpdateOps: DBUpdate<Error = Self::Error, NoteRef = Self::NoteRef, TxRef = Self::TxRef>;
fn init_db(&self) -> Result<(), Self::Error>;
@ -43,6 +47,25 @@ pub trait DBOps {
fn block_height_extrema(&self) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error>;
fn get_target_and_anchor_heights(
&self,
) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
self.block_height_extrema().map(|heights| {
heights.map(|(min_height, max_height)| {
let target_height = max_height + 1;
// Select an anchor ANCHOR_OFFSET back from the target block,
// unless that would be before the earliest block we have.
let anchor_height = BlockHeight::from(cmp::max(
u32::from(target_height).saturating_sub(ANCHOR_OFFSET),
u32::from(min_height),
));
(target_height, anchor_height)
})
})
}
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error>;
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error>;
@ -64,9 +87,20 @@ pub trait DBOps {
params: &P,
) -> Result<Vec<ExtendedFullViewingKey>, Self::Error>;
fn is_valid_account_extfvk<P: consensus::Parameters>(
&self,
params: &P,
account: AccountId,
extfvk: &ExtendedFullViewingKey,
) -> Result<bool, Self::Error>;
fn get_balance(&self, account: AccountId) -> Result<Amount, Self::Error>;
fn get_verified_balance(&self, account: AccountId) -> Result<Amount, Self::Error>;
fn get_verified_balance(
&self,
account: AccountId,
anchor_height: BlockHeight,
) -> Result<Amount, Self::Error>;
fn get_received_memo_as_utf8(
&self,
@ -89,15 +123,22 @@ pub trait DBOps {
fn get_update_ops(&self) -> Result<Self::UpdateOps, Self::Error>;
fn transactionally<F>(&self, mutator: &mut Self::UpdateOps, f: F) -> Result<(), Self::Error>
fn select_spendable_notes(
&self,
account: AccountId,
target_value: Amount,
anchor_height: BlockHeight,
) -> Result<Vec<SpendableNote>, Self::Error>;
fn transactionally<F, A>(&self, mutator: &mut Self::UpdateOps, f: F) -> Result<A, Self::Error>
where
F: FnOnce(&mut Self::UpdateOps) -> Result<(), Self::Error>;
F: FnOnce(&mut Self::UpdateOps) -> Result<A, Self::Error>;
}
pub trait DBUpdate {
type Error;
type TxRef: Copy;
type NoteRef: Copy;
type TxRef: Copy;
fn insert_block(
&mut self,
@ -113,7 +154,11 @@ pub trait DBUpdate {
height: BlockHeight,
) -> Result<Self::TxRef, Self::Error>;
fn put_tx_data(&mut self, tx: &Transaction) -> Result<Self::TxRef, Self::Error>;
fn put_tx_data(
&mut self,
tx: &Transaction,
created_at: Option<time::OffsetDateTime>,
) -> Result<Self::TxRef, Self::Error>;
fn mark_spent(&mut self, tx_ref: Self::TxRef, nf: &[u8]) -> Result<(), Self::Error>;

View File

@ -1,15 +1,26 @@
//! Functions for scanning the chain and extracting relevant information.
use zcash_primitives::{
consensus::{self, NetworkUpgrade},
transaction::Transaction,
consensus::{self, BranchId, NetworkUpgrade},
note_encryption::Memo,
prover::TxProver,
transaction::{
builder::Builder,
components::{amount::DEFAULT_FEE, Amount},
Transaction,
},
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
};
use crate::{
address::RecipientAddress,
data_api::{error::Error, DBOps, DBUpdate},
decrypt_transaction,
wallet::{AccountId, OvkPolicy},
};
pub const ANCHOR_OFFSET: u32 = 10;
/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
/// the wallet, and saves it to the wallet.
pub fn decrypt_and_store_transaction<'db, E0, N, E, P, D>(
@ -43,7 +54,7 @@ where
// Update the database atomically, to ensure the result is internally consistent.
data.transactionally(&mut db_update, |up| {
let tx_ref = up.put_tx_data(tx)?;
let tx_ref = up.put_tx_data(tx, None)?;
for output in outputs {
if output.outgoing {
@ -57,3 +68,201 @@ where
})
}
}
/// Creates a transaction paying the specified address from the given account.
///
/// Returns the row index of the newly-created transaction in the `transactions` table
/// within the data database. The caller can read the raw transaction bytes from the `raw`
/// column in order to broadcast the transaction to the network.
///
/// Do not call this multiple times in parallel, or you will generate transactions that
/// double-spend the same notes.
///
/// # Transaction privacy
///
/// `ovk_policy` specifies the desired policy for which outgoing viewing key should be
/// able to decrypt the outputs of this transaction. This is primarily relevant to
/// wallet recovery from backup; in particular, [`OvkPolicy::Discard`] will prevent the
/// recipient's address, and the contents of `memo`, from ever being recovered from the
/// block chain. (The total value sent can always be inferred by the sender from the spent
/// notes and received change.)
///
/// Regardless of the specified policy, `create_spend_to_address` saves `to`, `value`, and
/// `memo` in `db_data`. This can be deleted independently of `ovk_policy`.
///
/// For details on what transaction information is visible to the holder of a full or
/// outgoing viewing key, refer to [ZIP 310].
///
/// [ZIP 310]: https://zips.z.cash/zip-0310
///
/// # Examples
///
/// ```
/// use tempfile::NamedTempFile;
/// use zcash_primitives::{
/// consensus::{self, Network},
/// constants::testnet::COIN_TYPE,
/// transaction::components::Amount
/// };
/// use zcash_proofs::prover::LocalTxProver;
/// use zcash_client_backend::{
/// api::AccountId,
/// keys::spending_key,
/// data_api::wallet::create_spend_to_address,
/// wallet::OvkPolicy,
/// };
/// use zcash_client_sqlite::{
/// DataConnection,
/// };
///
/// let tx_prover = 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.");
/// }
/// };
///
/// let account = AccountId(0);
/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, account.0);
/// let to = extsk.default_address().unwrap().1.into();
///
/// let data_file = NamedTempFile::new().unwrap();
/// let db = DataConnection::for_path(data_file).unwrap();
/// match create_spend_to_address(
/// &db,
/// &Network::TestNetwork,
/// tx_prover,
/// account,
/// &extsk,
/// &to,
/// Amount::from_u64(1).unwrap(),
/// None,
/// OvkPolicy::Sender,
/// ) {
/// Ok(tx_row) => (),
/// Err(e) => (),
/// }
/// ```
pub fn create_spend_to_address<'db, E0, N, E, P, D, R>(
data: &'db D,
params: &P,
prover: impl TxProver,
account: AccountId,
extsk: &ExtendedSpendingKey,
to: &RecipientAddress,
value: Amount,
memo: Option<Memo>,
ovk_policy: OvkPolicy,
) -> Result<R, Error<E, N>>
where
E0: Into<Error<E, N>>,
P: consensus::Parameters + Clone,
R: Copy,
&'db D: DBOps<Error = E0, TxRef = R>,
{
// Check that the ExtendedSpendingKey we have been given corresponds to the
// ExtendedFullViewingKey for the account we are spending from.
let extfvk = ExtendedFullViewingKey::from(extsk);
if !data
.is_valid_account_extfvk(params, account, &extfvk)
.map_err(|e| e.into())?
{
return Err(Error::InvalidExtSK(account));
}
// Apply the outgoing viewing key policy.
let ovk = match ovk_policy {
OvkPolicy::Sender => Some(extfvk.fvk.ovk),
OvkPolicy::Custom(ovk) => Some(ovk),
OvkPolicy::Discard => None,
};
// Target the next block, assuming we are up-to-date.
let (height, anchor_height) = data
.get_target_and_anchor_heights()
.map_err(|e| e.into())
.and_then(|x| x.ok_or(Error::ScanRequired))?;
let target_value = value + DEFAULT_FEE;
let spendable_notes = data
.select_spendable_notes(account, target_value, anchor_height)
.map_err(|e| e.into())?;
// Confirm we were able to select sufficient value
let selected_value = spendable_notes.iter().map(|n| n.note_value).sum();
if selected_value < target_value {
return Err(Error::InsufficientBalance(selected_value, target_value));
}
// Create the transaction
let mut builder = Builder::new(params.clone(), height);
for selected in spendable_notes {
let from = extfvk
.fvk
.vk
.to_payment_address(selected.diversifier)
.unwrap(); //JUBJUB would have to unexpectedly be the zero point for this to be None
let note = from
.create_note(u64::from(selected.note_value), selected.rseed)
.unwrap();
let merkle_path = selected.witness.path().expect("the tree is not empty");
builder
.add_sapling_spend(extsk.clone(), selected.diversifier, note, merkle_path)
.map_err(Error::Builder)?;
}
match to {
RecipientAddress::Shielded(to) => {
builder.add_sapling_output(ovk, to.clone(), value, memo.clone())
}
RecipientAddress::Transparent(to) => builder.add_transparent_output(&to, value),
}?;
let consensus_branch_id = BranchId::for_height(params, height);
let (tx, tx_metadata) = builder
.build(consensus_branch_id, &prover)
.map_err(Error::Builder)?;
// We only called add_sapling_output() once.
let output_index = match tx_metadata.output_index(0) {
Some(idx) => idx as i64,
None => panic!("Output 0 should exist in the transaction"),
};
// Update the database atomically, to ensure the result is internally consistent.
let mut db_update = data.get_update_ops().map_err(|e| e.into())?;
data.transactionally(&mut db_update, |up| {
let created = time::OffsetDateTime::now_utc();
let tx_ref = up.put_tx_data(&tx, Some(created))?;
// Mark notes as spent.
//
// This locks the notes so they aren't selected again by a subsequent call to
// create_spend_to_address() before this transaction has been mined (at which point the notes
// get re-marked as spent).
//
// Assumes that create_spend_to_address() will never be called in parallel, which is a
// reasonable assumption for a light client such as a mobile phone.
for spend in &tx.shielded_spends {
up.mark_spent(tx_ref, &spend.nullifier)?;
}
up.insert_sent_note(
params,
tx_ref,
output_index as usize,
account,
to,
value,
memo,
)?;
// Return the row number of the transaction, so the caller can fetch it for sending.
Ok(tx_ref)
})
.map_err(|e| e.into())
}

View File

@ -2,10 +2,11 @@
//! light client.
use zcash_primitives::{
keys::OutgoingViewingKey,
merkle_tree::IncrementalWitness,
primitives::{Note, PaymentAddress},
primitives::{Diversifier, Note, PaymentAddress, Rseed},
sapling::Node,
transaction::TxId,
transaction::{components::Amount, TxId},
};
/// A type-safe wrapper for account identifiers.
@ -46,3 +47,36 @@ pub struct WalletShieldedOutput {
pub is_change: bool,
pub witness: IncrementalWitness<Node>,
}
pub struct SpendableNote {
pub diversifier: Diversifier,
pub note_value: Amount,
pub rseed: Rseed,
pub witness: IncrementalWitness<Node>,
}
/// Describes a policy for which outgoing viewing key should be able to decrypt
/// transaction outputs.
///
/// For details on what transaction information is visible to the holder of an outgoing
/// viewing key, refer to [ZIP 310].
///
/// [ZIP 310]: https://zips.z.cash/zip-0310
pub enum OvkPolicy {
/// Use the outgoing viewing key from the sender's [`ExtendedFullViewingKey`].
///
/// Transaction outputs will be decryptable by the sender, in addition to the
/// recipients.
Sender,
/// Use a custom outgoing viewing key. This might for instance be derived from a
/// separate seed than the wallet's spending keys.
///
/// Transaction outputs will be decryptable by the recipients, and whoever controls
/// the provided outgoing viewing key.
Custom(OutgoingViewingKey),
/// Use no outgoing viewing key. Transaction outputs will be decryptable by their
/// recipients, but not by the sender.
Discard,
}

View File

@ -56,3 +56,9 @@ impl From<protobuf::ProtobufError> for SqliteClientError {
SqliteClientError(Error::Protobuf(e))
}
}
impl From<SqliteClientError> for Error<rusqlite::Error, NoteId> {
fn from(e: SqliteClientError) -> Self {
e.0
}
}

View File

@ -47,7 +47,7 @@ use zcash_client_backend::{
data_api::{error::Error, CacheOps, DBOps, DBUpdate, ShieldedOutput},
encoding::encode_payment_address,
proto::compact_formats::CompactBlock,
wallet::{AccountId, WalletTx},
wallet::{AccountId, SpendableNote, WalletTx},
DecryptedOutput,
};
@ -77,6 +77,7 @@ impl DataConnection {
impl<'a> DBOps for &'a DataConnection {
type Error = SqliteClientError;
type NoteRef = NoteId;
type TxRef = i64;
type UpdateOps = DataConnStmtCache<'a>;
fn init_db(&self) -> Result<(), Self::Error> {
@ -121,6 +122,13 @@ impl<'a> DBOps for &'a DataConnection {
wallet::rewind_to_height(self, parameters, block_height)
}
fn get_extended_full_viewing_keys<P: consensus::Parameters>(
&self,
params: &P,
) -> Result<Vec<ExtendedFullViewingKey>, Self::Error> {
wallet::get_extended_full_viewing_keys(self, params)
}
fn get_address<P: consensus::Parameters>(
&self,
params: &P,
@ -129,12 +137,25 @@ impl<'a> DBOps for &'a DataConnection {
wallet::get_address(self, params, account)
}
fn is_valid_account_extfvk<P: consensus::Parameters>(
&self,
params: &P,
account: AccountId,
extfvk: &ExtendedFullViewingKey,
) -> Result<bool, Self::Error> {
wallet::is_valid_account_extfvk(self, params, account, extfvk)
}
fn get_balance(&self, account: AccountId) -> Result<Amount, Self::Error> {
wallet::get_balance(self, account)
}
fn get_verified_balance(&self, account: AccountId) -> Result<Amount, Self::Error> {
wallet::get_verified_balance(self, account)
fn get_verified_balance(
&self,
account: AccountId,
anchor_height: BlockHeight,
) -> Result<Amount, Self::Error> {
wallet::get_verified_balance(self, account, anchor_height)
}
fn get_received_memo_as_utf8(
@ -148,13 +169,6 @@ impl<'a> DBOps for &'a DataConnection {
wallet::get_sent_memo_as_utf8(self, id_note)
}
fn get_extended_full_viewing_keys<P: consensus::Parameters>(
&self,
params: &P,
) -> Result<Vec<ExtendedFullViewingKey>, Self::Error> {
wallet::get_extended_full_viewing_keys(self, params)
}
fn get_commitment_tree(
&self,
block_height: BlockHeight,
@ -173,6 +187,20 @@ impl<'a> DBOps for &'a DataConnection {
wallet::get_nullifiers(self)
}
fn select_spendable_notes(
&self,
account: AccountId,
target_value: Amount,
anchor_height: BlockHeight,
) -> Result<Vec<SpendableNote>, Self::Error> {
wallet::transact::select_spendable_notes(
self,
account,
target_value,
anchor_height,
)
}
fn get_update_ops(&self) -> Result<Self::UpdateOps, Self::Error> {
Ok(
DataConnStmtCache {
@ -190,8 +218,8 @@ impl<'a> DBOps for &'a DataConnection {
SET block = ?, tx_index = ? WHERE txid = ?",
)?,
stmt_insert_tx_data: self.0.prepare(
"INSERT INTO transactions (txid, expiry_height, raw)
VALUES (?, ?, ?)",
"INSERT INTO transactions (txid, created, expiry_height, raw)
VALUES (?, ?, ?, ?)",
)?,
stmt_update_tx_data: self.0.prepare(
"UPDATE transactions
@ -210,12 +238,12 @@ impl<'a> DBOps for &'a DataConnection {
stmt_update_received_note: self.0.prepare(
"UPDATE received_notes
SET account = :account,
diversifier = :diversifier,
value = :value,
rcm = :rcm,
nf = IFNULL(:memo, nf),
memo = IFNULL(:nf, memo),
is_change = :is_change
diversifier = :diversifier,
value = :value,
rcm = :rcm,
nf = IFNULL(:memo, nf),
memo = IFNULL(:nf, memo),
is_change = :is_change
WHERE tx = :tx AND output_index = :output_index",
)?,
stmt_select_received_note: self.0.prepare(
@ -247,27 +275,28 @@ impl<'a> DBOps for &'a DataConnection {
)
}
fn transactionally<F>(&self, mutator: &mut Self::UpdateOps, f: F) -> Result<(), Self::Error>
fn transactionally<F, A>(&self, mutator: &mut Self::UpdateOps, f: F) -> Result<A, Self::Error>
where
F: FnOnce(&mut Self::UpdateOps) -> Result<(), Self::Error>,
F: FnOnce(&mut Self::UpdateOps) -> Result<A, Self::Error>,
{
self.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
match f(mutator) {
Ok(_) => {
Ok(result) => {
self.0.execute("COMMIT", NO_PARAMS)?;
Ok(())
Ok(result)
}
Err(error) => {
match self.0.execute("ROLLBACK", NO_PARAMS) {
Ok(_) => Err(error),
// REVIEW: If rollback fails, what do we want to do? I think that
// panicking here is probably the right thing to do, because it
// means the database is corrupt?
Err(e) => panic!(
"Rollback failed with error {} while attempting to recover from error {}; database is likely corrupt.",
e,
error.0
)
Err(e) =>
// REVIEW: If rollback fails, what do we want to do? I think that
// panicking here is probably the right thing to do, because it
// means the database is corrupt?
panic!(
"Rollback failed with error {} while attempting to recover from error {}; database is likely corrupt.",
e,
error.0
)
}
}
}
@ -355,7 +384,11 @@ impl<'a> DBUpdate for DataConnStmtCache<'a> {
}
}
fn put_tx_data(&mut self, tx: &Transaction) -> Result<Self::TxRef, Self::Error> {
fn put_tx_data(
&mut self,
tx: &Transaction,
created_at: Option<time::OffsetDateTime>,
) -> Result<Self::TxRef, Self::Error> {
let txid = tx.txid().0.to_vec();
let mut raw_tx = vec![];
@ -371,6 +404,7 @@ impl<'a> DBUpdate for DataConnStmtCache<'a> {
self.stmt_insert_tx_data.execute(&[
txid.to_sql()?,
u32::from(tx.expiry_height).to_sql()?,
created_at.to_sql()?,
raw_tx.to_sql()?,
])?;

View File

@ -1,6 +1,6 @@
//! Functions for querying information in the data database.
use rusqlite::{OptionalExtension, NO_PARAMS};
use rusqlite::{OptionalExtension, ToSql, NO_PARAMS};
use zcash_primitives::{
block::BlockHash,
@ -14,8 +14,10 @@ use zcash_primitives::{
};
use zcash_client_backend::{
data_api::{chain::get_target_and_anchor_heights, error::Error},
encoding::{decode_extended_full_viewing_key, decode_payment_address},
data_api::error::Error,
encoding::{
decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key,
},
};
use crate::{error::SqliteClientError, AccountId, DataConnection, NoteId};
@ -58,6 +60,51 @@ pub fn get_address<P: consensus::Parameters>(
.map_err(|e| SqliteClientError(e.into()))
}
pub fn get_extended_full_viewing_keys<P: consensus::Parameters>(
data: &DataConnection,
params: &P,
) -> Result<Vec<ExtendedFullViewingKey>, SqliteClientError> {
// Fetch the ExtendedFullViewingKeys we are tracking
let mut stmt_fetch_accounts = data
.0
.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
let rows = stmt_fetch_accounts
.query_map(NO_PARAMS, |row| {
row.get(0).map(|extfvk: String| {
decode_extended_full_viewing_key(
params.hrp_sapling_extended_full_viewing_key(),
&extfvk,
)
.map_err(|e| Error::Bech32(e))
.and_then(|k| k.ok_or(Error::IncorrectHRPExtFVK))
.map_err(SqliteClientError)
})
})
.map_err(SqliteClientError::from)?;
rows.collect::<Result<Result<_, _>, _>>()?
}
pub fn is_valid_account_extfvk<P: consensus::Parameters>(
data: &DataConnection,
params: &P,
account: AccountId,
extfvk: &ExtendedFullViewingKey,
) -> Result<bool, SqliteClientError> {
data.0
.prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")?
.exists(&[
account.0.to_sql()?,
encode_extended_full_viewing_key(
params.hrp_sapling_extended_full_viewing_key(),
extfvk,
)
.to_sql()?,
])
.map_err(SqliteClientError::from)
}
/// Returns the balance for the account, including all mined unspent notes that we know
/// about.
///
@ -117,9 +164,8 @@ pub fn get_balance(data: &DataConnection, account: AccountId) -> Result<Amount,
pub fn get_verified_balance(
data: &DataConnection,
account: AccountId,
anchor_height: BlockHeight,
) -> Result<Amount, SqliteClientError> {
let (_, anchor_height) = get_target_and_anchor_heights(data)?;
let balance = data.0.query_row(
"SELECT SUM(value) FROM received_notes
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
@ -320,31 +366,6 @@ pub fn rewind_to_height<P: consensus::Parameters>(
Ok(())
}
pub fn get_extended_full_viewing_keys<P: consensus::Parameters>(
data: &DataConnection,
params: &P,
) -> Result<Vec<ExtendedFullViewingKey>, SqliteClientError> {
// Fetch the ExtendedFullViewingKeys we are tracking
let mut stmt_fetch_accounts = data
.0
.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
let rows = stmt_fetch_accounts
.query_map(NO_PARAMS, |row| {
row.get(0).map(|extfvk: String| {
decode_extended_full_viewing_key(
params.hrp_sapling_extended_full_viewing_key(),
&extfvk,
)
.map_err(|e| Error::Bech32(e))
.and_then(|k| k.ok_or(Error::IncorrectHRPExtFVK))
.map_err(SqliteClientError)
})
})
.map_err(SqliteClientError::from)?;
rows.collect::<Result<Result<_, _>, _>>()?
}
pub fn get_commitment_tree(
data: &DataConnection,
@ -424,7 +445,7 @@ mod tests {
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
};
use zcash_client_backend::data_api::error::Error;
use zcash_client_backend::data_api::{error::Error, DBOps};
use crate::{
tests,
@ -449,7 +470,8 @@ mod tests {
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero());
// The account should have no verified balance, as we haven't scanned any blocks
let e = get_verified_balance(&db_data, AccountId(0)).unwrap_err();
let (_, anchor_height) = (&db_data).get_target_and_anchor_heights().unwrap().unwrap();
let e = get_verified_balance(&db_data, AccountId(0), anchor_height).unwrap_err();
match e.0 {
Error::ScanRequired => (),
_ => panic!("Unexpected error: {:?}", e),

View File

@ -1,176 +1,29 @@
//! Functions for creating transactions.
use ff::PrimeField;
use rusqlite::{types::ToSql, NO_PARAMS};
//!
use std::convert::TryInto;
use ff::PrimeField;
use zcash_primitives::{
consensus,
keys::OutgoingViewingKey,
merkle_tree::{IncrementalWitness, MerklePath},
note_encryption::Memo,
primitives::{Diversifier, Note, Rseed},
prover::TxProver,
sapling::Node,
transaction::{
builder::Builder,
components::{amount::DEFAULT_FEE, Amount},
},
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
consensus::{BlockHeight},
merkle_tree::IncrementalWitness,
primitives::{Diversifier, Rseed},
transaction::components::Amount,
};
use zcash_client_backend::{
address::RecipientAddress,
data_api::{chain::get_target_and_anchor_heights, error::Error, DBOps, DBUpdate},
encoding::encode_extended_full_viewing_key,
wallet::AccountId,
data_api::{error::Error},
wallet::{AccountId, SpendableNote},
};
use crate::{error::SqliteClientError, DataConnection};
/// Describes a policy for which outgoing viewing key should be able to decrypt
/// transaction outputs.
///
/// For details on what transaction information is visible to the holder of an outgoing
/// viewing key, refer to [ZIP 310].
///
/// [ZIP 310]: https://zips.z.cash/zip-0310
pub enum OvkPolicy {
/// Use the outgoing viewing key from the sender's [`ExtendedFullViewingKey`].
///
/// Transaction outputs will be decryptable by the sender, in addition to the
/// recipients.
Sender,
/// Use a custom outgoing viewing key. This might for instance be derived from a
/// separate seed than the wallet's spending keys.
///
/// Transaction outputs will be decryptable by the recipients, and whoever controls
/// the provided outgoing viewing key.
Custom(OutgoingViewingKey),
/// Use no outgoing viewing key. Transaction outputs will be decryptable by their
/// recipients, but not by the sender.
Discard,
}
struct SelectedNoteRow {
diversifier: Diversifier,
note: Note,
merkle_path: MerklePath<Node>,
}
/// Creates a transaction paying the specified address from the given account.
///
/// Returns the row index of the newly-created transaction in the `transactions` table
/// within the data database. The caller can read the raw transaction bytes from the `raw`
/// column in order to broadcast the transaction to the network.
///
/// Do not call this multiple times in parallel, or you will generate transactions that
/// double-spend the same notes.
///
/// # Transaction privacy
///
/// `ovk_policy` specifies the desired policy for which outgoing viewing key should be
/// able to decrypt the outputs of this transaction. This is primarily relevant to
/// wallet recovery from backup; in particular, [`OvkPolicy::Discard`] will prevent the
/// recipient's address, and the contents of `memo`, from ever being recovered from the
/// block chain. (The total value sent can always be inferred by the sender from the spent
/// notes and received change.)
///
/// Regardless of the specified policy, `create_to_address` saves `to`, `value`, and
/// `memo` in `db_data`. This can be deleted independently of `ovk_policy`.
///
/// For details on what transaction information is visible to the holder of a full or
/// outgoing viewing key, refer to [ZIP 310].
///
/// [ZIP 310]: https://zips.z.cash/zip-0310
///
/// # Examples
///
/// ```
/// use tempfile::NamedTempFile;
/// use zcash_primitives::{
/// consensus::{self, Network},
/// constants::testnet::COIN_TYPE,
/// transaction::components::Amount
/// };
/// use zcash_proofs::prover::LocalTxProver;
/// use zcash_client_backend::{
/// keys::spending_key,
/// };
/// use zcash_client_sqlite::{
/// DataConnection,
/// transact::{create_to_address, OvkPolicy},
/// };
///
/// let tx_prover = 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.");
/// }
/// };
///
/// let account = 0;
/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, account);
/// let to = extsk.default_address().unwrap().1.into();
///
/// let data_file = NamedTempFile::new().unwrap();
/// let db = DataConnection::for_path(data_file).unwrap();
/// match create_to_address(
/// &db,
/// &Network::TestNetwork,
/// consensus::BranchId::Sapling,
/// tx_prover,
/// (account, &extsk),
/// &to,
/// Amount::from_u64(1).unwrap(),
/// None,
/// OvkPolicy::Sender,
/// ) {
/// Ok(tx_row) => (),
/// Err(e) => (),
/// }
/// ```
pub fn create_to_address<P: consensus::Parameters>(
pub fn select_spendable_notes(
data: &DataConnection,
params: &P,
consensus_branch_id: consensus::BranchId,
prover: impl TxProver,
(account, extsk): (u32, &ExtendedSpendingKey),
to: &RecipientAddress,
value: Amount,
memo: Option<Memo>,
ovk_policy: OvkPolicy,
) -> Result<i64, SqliteClientError> {
// Check that the ExtendedSpendingKey we have been given corresponds to the
// ExtendedFullViewingKey for the account we are spending from.
let extfvk = ExtendedFullViewingKey::from(extsk);
if !data
.0
.prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")?
.exists(&[
account.to_sql()?,
encode_extended_full_viewing_key(
params.hrp_sapling_extended_full_viewing_key(),
&extfvk,
)
.to_sql()?,
])?
{
return Err(Error::InvalidExtSK(account).into());
}
// Apply the outgoing viewing key policy.
let ovk = match ovk_policy {
OvkPolicy::Sender => Some(extfvk.fvk.ovk),
OvkPolicy::Custom(ovk) => Some(ovk),
OvkPolicy::Discard => None,
};
// Target the next block, assuming we are up-to-date.
let (height, anchor_height) = get_target_and_anchor_heights(data)?;
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:
@ -189,7 +42,6 @@ pub fn create_to_address<P: consensus::Parameters>(
// 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 target_value = i64::from(value + DEFAULT_FEE);
let mut stmt_select_notes = data.0.prepare(
"WITH selected AS (
WITH eligible AS (
@ -198,14 +50,14 @@ pub fn create_to_address<P: consensus::Parameters>(
(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 = ? AND spent IS NULL AND transactions.block <= ?
WHERE account = :account AND spent IS NULL AND transactions.block <= :anchor_height
)
SELECT * FROM eligible WHERE so_far < ?
SELECT * FROM eligible WHERE so_far < :target_value
UNION
SELECT * FROM (SELECT * FROM eligible WHERE so_far >= ? LIMIT 1)
SELECT * FROM (SELECT * FROM eligible WHERE so_far >= :target_value LIMIT 1)
), witnesses AS (
SELECT note, witness FROM sapling_witnesses
WHERE block = ?
WHERE block = :anchor_height
)
SELECT selected.diversifier, selected.value, selected.rcm, witnesses.witness
FROM selected
@ -213,13 +65,11 @@ pub fn create_to_address<P: consensus::Parameters>(
)?;
// Select notes
let notes = stmt_select_notes.query_and_then::<_, SqliteClientError, _, _>(
let notes = stmt_select_notes.query_and_then_named::<_, SqliteClientError, _>(
&[
i64::from(account),
i64::from(anchor_height),
target_value,
target_value,
i64::from(anchor_height),
(&"account", &i64::from(account.0)),
(&"anchor_height", &u32::from(anchor_height)),
(&"target_value", &i64::from(target_value)),
],
|row| {
let diversifier = {
@ -234,7 +84,7 @@ pub fn create_to_address<P: consensus::Parameters>(
Diversifier(tmp)
};
let note_value: i64 = row.get(1)?;
let note_value = Amount::from_i64(row.get(1)?).unwrap();
let rseed = {
let rcm_bytes: Vec<_> = row.get(2)?;
@ -251,104 +101,22 @@ pub fn create_to_address<P: consensus::Parameters>(
Rseed::BeforeZip212(rcm)
};
let from = extfvk.fvk.vk.to_payment_address(diversifier).unwrap();
let note = from.create_note(note_value as u64, rseed).unwrap();
let merkle_path = {
let witness = {
let d: Vec<_> = row.get(3)?;
IncrementalWitness::read(&d[..])?
.path()
.expect("the tree is not empty")
};
Ok(SelectedNoteRow {
Ok(SpendableNote {
diversifier,
note,
merkle_path,
note_value,
rseed,
witness,
})
},
)?;
let notes: Vec<SelectedNoteRow> = notes.collect::<Result<_, _>>()?;
// Confirm we were able to select sufficient value
let selected_value = notes
.iter()
.fold(0, |acc, selected| acc + selected.note.value);
if selected_value < target_value as u64 {
return Err(Error::InsufficientBalance(selected_value, target_value as u64).into());
}
// Create the transaction
let mut builder = Builder::new(params.clone(), height);
for selected in notes {
builder.add_sapling_spend(
extsk.clone(),
selected.diversifier,
selected.note,
selected.merkle_path,
)?;
}
match to {
RecipientAddress::Shielded(to) => {
builder.add_sapling_output(ovk, to.clone(), value, memo.clone())
}
RecipientAddress::Transparent(to) => builder.add_transparent_output(&to, value),
}?;
let (tx, tx_metadata) = builder.build(consensus_branch_id, &prover)?;
// We only called add_sapling_output() once.
let output_index = match tx_metadata.output_index(0) {
Some(idx) => idx as i64,
None => panic!("Output 0 should exist in the transaction"),
};
let created = time::OffsetDateTime::now_utc();
// Update the database atomically, to ensure the result is internally consistent.
data.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
let mut db_update = data.get_update_ops()?;
// Save the transaction in the database.
let mut raw_tx = vec![];
tx.write(&mut raw_tx)?;
let mut stmt_insert_tx = data.0.prepare(
"INSERT INTO transactions (txid, created, expiry_height, raw)
VALUES (?, ?, ?, ?)",
)?;
stmt_insert_tx.execute(&[
tx.txid().0.to_sql()?,
created.to_sql()?,
i64::from(tx.expiry_height).to_sql()?,
raw_tx.to_sql()?,
])?;
let tx_ref = data.0.last_insert_rowid();
// Mark notes as spent.
//
// This locks the notes so they aren't selected again by a subsequent call to
// create_to_address() before this transaction has been mined (at which point the notes
// get re-marked as spent).
//
// Assumes that create_to_address() will never be called in parallel, which is a
// reasonable assumption for a light client such as a mobile phone.
for spend in &tx.shielded_spends {
db_update.mark_spent(tx_ref, &spend.nullifier)?;
}
// Save the sent note in the database.
// TODO: Decide how to save transparent output information.
db_update.insert_sent_note(
params,
tx_ref,
output_index as usize,
AccountId(account),
to,
value,
memo,
)?;
data.0.execute("COMMIT", NO_PARAMS)?;
// Return the row number of the transaction, so the caller can fetch it for sending.
Ok(tx_ref)
let notes: Vec<SpendableNote> = notes.collect::<Result<_, _>>()?;
Ok(notes)
}
#[cfg(test)]
@ -358,7 +126,7 @@ mod tests {
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
consensus::BlockHeight,
note_encryption::try_sapling_output_recovery,
prover::TxProver,
transaction::{components::Amount, Transaction},
@ -367,7 +135,10 @@ mod tests {
use zcash_proofs::prover::LocalTxProver;
use zcash_client_backend::data_api::chain::scan_cached_blocks;
use zcash_client_backend::{
data_api::{chain::scan_cached_blocks, wallet::create_spend_to_address, DBOps},
wallet::OvkPolicy,
};
use crate::{
chain::init::init_cache_database,
@ -379,8 +150,6 @@ mod tests {
AccountId, CacheConnection, DataConnection,
};
use super::{create_to_address, OvkPolicy};
fn test_prover() -> impl TxProver {
match LocalTxProver::with_default_location() {
Some(tx_prover) => tx_prover,
@ -407,12 +176,12 @@ mod tests {
let to = extsk0.default_address().unwrap().1.into();
// Invalid extsk for the given account should cause an error
match create_to_address(
match create_spend_to_address(
&db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk1),
AccountId(0),
&extsk1,
&to,
Amount::from_u64(1).unwrap(),
None,
@ -422,12 +191,12 @@ mod tests {
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"),
}
match create_to_address(
match create_spend_to_address(
&db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(1, &extsk0),
AccountId(1),
&extsk0,
&to,
Amount::from_u64(1).unwrap(),
None,
@ -451,12 +220,12 @@ mod tests {
let to = extsk.default_address().unwrap().1.into();
// We cannot do anything if we aren't synchronised
match create_to_address(
match create_spend_to_address(
&db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
AccountId(0),
&extsk,
&to,
Amount::from_u64(1).unwrap(),
None,
@ -491,12 +260,12 @@ mod tests {
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero());
// We cannot spend anything
match create_to_address(
match create_spend_to_address(
&db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
AccountId(0),
&extsk,
&to,
Amount::from_u64(1).unwrap(),
None,
@ -537,8 +306,12 @@ mod tests {
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
// Verified balance matches total balance
let (_, anchor_height) = (&db_data).get_target_and_anchor_heights().unwrap().unwrap();
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value);
assert_eq!(get_verified_balance(&db_data, AccountId(0)).unwrap(), value);
assert_eq!(
get_verified_balance(&db_data, AccountId(0), anchor_height).unwrap(),
value
);
// Add more funds to the wallet in a second note
let (cb, _) = fake_compact_block(
@ -551,18 +324,22 @@ mod tests {
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
// Verified balance does not include the second note
let (_, anchor_height2) = (&db_data).get_target_and_anchor_heights().unwrap().unwrap();
assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value + value);
assert_eq!(get_verified_balance(&db_data, AccountId(0)).unwrap(), value);
assert_eq!(
get_verified_balance(&db_data, AccountId(0), anchor_height2).unwrap(),
value
);
// Spend fails because there are insufficient verified notes
let extsk2 = ExtendedSpendingKey::master(&[]);
let to = extsk2.default_address().unwrap().1.into();
match create_to_address(
match create_spend_to_address(
&db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
AccountId(0),
&extsk,
&to,
Amount::from_u64(70000).unwrap(),
None,
@ -589,12 +366,12 @@ mod tests {
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
// Second spend still fails
match create_to_address(
match create_spend_to_address(
&db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
AccountId(0),
&extsk,
&to,
Amount::from_u64(70000).unwrap(),
None,
@ -618,12 +395,12 @@ mod tests {
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
// Second spend should now succeed
create_to_address(
create_spend_to_address(
&db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
AccountId(0),
&extsk,
&to,
Amount::from_u64(70000).unwrap(),
None,
@ -662,12 +439,12 @@ mod tests {
// Send some of the funds to another address
let extsk2 = ExtendedSpendingKey::master(&[]);
let to = extsk2.default_address().unwrap().1.into();
create_to_address(
create_spend_to_address(
&db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
AccountId(0),
&extsk,
&to,
Amount::from_u64(15000).unwrap(),
None,
@ -676,12 +453,12 @@ mod tests {
.unwrap();
// A second spend fails because there are no usable notes
match create_to_address(
match create_spend_to_address(
&db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
AccountId(0),
&extsk,
&to,
Amount::from_u64(2000).unwrap(),
None,
@ -708,12 +485,12 @@ mod tests {
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
// Second spend still fails
match create_to_address(
match create_spend_to_address(
&db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
AccountId(0),
&extsk,
&to,
Amount::from_u64(2000).unwrap(),
None,
@ -737,12 +514,12 @@ mod tests {
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
// Second spend should now succeed
create_to_address(
create_spend_to_address(
&db_data,
&tests::network(),
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
AccountId(0),
&extsk,
&to,
Amount::from_u64(2000).unwrap(),
None,
@ -784,12 +561,12 @@ mod tests {
let to = addr2.clone().into();
let send_and_recover_with_policy = |ovk_policy| {
let tx_row = create_to_address(
let tx_row = create_spend_to_address(
&db_data,
&network,
consensus::BranchId::Blossom,
test_prover(),
(0, &extsk),
AccountId(0),
&extsk,
&to,
Amount::from_u64(15000).unwrap(),
None,