Query for unspent utxos checks to ensure that spending tx is mined.

Also make it an error to try to send a memo to a transparent address.
This commit is contained in:
Kris Nuttycombe 2021-03-26 18:39:43 -06:00
parent b88ee47e36
commit 8828276361
7 changed files with 119 additions and 35 deletions

View File

@ -22,6 +22,7 @@ group = "0.8"
hex = "0.4"
hdwallet = { version = "0.3.0", optional = true }
jubjub = "0.5.1"
log = "0.4"
nom = "6.1"
percent-encoding = "2.1.0"
proptest = { version = "0.10.1", optional = true }
@ -29,7 +30,7 @@ protobuf = "2.20"
rand_core = "0.5.1"
ripemd160 = { version = "0.9.1", optional = true }
secp256k1 = { version = "0.19", optional = true }
sha2 = "0.9"
sha2 = { version = "0.9", optional = true }
subtle = "2.2.3"
time = "0.2"
zcash_note_encryption = { version = "0.0", path = "../components/zcash_note_encryption" }
@ -47,8 +48,8 @@ zcash_client_sqlite = { version = "0.3", path = "../zcash_client_sqlite" }
zcash_proofs = { version = "0.5", path = "../zcash_proofs" }
[features]
transparent-inputs = ["ripemd160", "secp256k1"]
test-dependencies = ["proptest", "zcash_primitives/test-dependencies", "hdwallet"]
transparent-inputs = ["ripemd160", "hdwallet", "sha2", "secp256k1"]
test-dependencies = ["proptest", "zcash_primitives/test-dependencies", "hdwallet", "sha2"]
[badges]
maintenance = { status = "actively-developed" }

View File

@ -59,6 +59,12 @@ pub enum Error<NoteId> {
/// The wallet attempted a sapling-only operation at a block
/// height when Sapling was not yet active.
SaplingNotActive,
/// A memo is required when constructing a Sapling output
MemoRequired,
/// It is forbidden to provide a memo when constructing a transparent output.
MemoForbidden,
}
impl ChainInvalid {
@ -99,6 +105,8 @@ impl<N: fmt::Display> fmt::Display for Error<N> {
Error::Builder(e) => write!(f, "{:?}", e),
Error::Protobuf(e) => write!(f, "{}", e),
Error::SaplingNotActive => write!(f, "Could not determine Sapling upgrade activation height."),
Error::MemoRequired => write!(f, "A memo is required when sending to a Sapling address."),
Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."),
}
}
}

View File

@ -1,6 +1,5 @@
//! Functions for scanning the chain and extracting relevant information.
use std::convert::TryFrom;
use std::fmt::Debug;
use zcash_primitives::{
consensus::{self, BranchId, NetworkUpgrade},
memo::MemoBytes,
@ -40,6 +39,8 @@ where
P: consensus::Parameters,
D: WalletWrite<Error = E>,
{
debug!("decrypt_and_store: {:?}", tx);
// Fetch the ExtendedFullViewingKeys we are tracking
let extfvks = data.get_extended_full_viewing_keys()?;
@ -54,16 +55,32 @@ where
.ok_or(Error::SaplingNotActive)?;
let outputs = decrypt_transaction(params, height, tx, &extfvks);
if outputs.is_empty() {
Ok(())
} else {
if !outputs.is_empty() {
data.store_received_tx(&ReceivedTransaction {
tx,
outputs: &outputs,
})?;
Ok(())
}
// store z->t transactions in the same way the would be stored by create_spend_to_address
if !tx.vout.is_empty() {
// TODO: clarify with Kris the simplest way to determine account and iterate over outputs
// i.e. there are probably edge cases where we need to combine vouts into one "sent" transaction for the total value
data.store_sent_tx(&SentTransaction {
tx: &tx,
created: time::OffsetDateTime::now_utc(),
output_index: usize::try_from(0).unwrap(),
account: AccountId(0),
recipient_address: &RecipientAddress::Transparent(
tx.vout[0].script_pubkey.address().unwrap(),
),
value: tx.vout[0].value,
memo: None,
utxos_spent: vec![],
})?;
}
Ok(())
}
#[allow(clippy::needless_doctest_main)]
@ -224,12 +241,22 @@ where
match to {
RecipientAddress::Shielded(to) => {
builder.add_sapling_output(ovk, to.clone(), value, memo.clone())
memo.clone().ok_or(Error::MemoRequired).and_then(|memo| {
builder
.add_sapling_output(ovk, to.clone(), value, memo)
.map_err(Error::Builder)
})
}
RecipientAddress::Transparent(to) => builder.add_transparent_output(&to, value),
}
.map_err(Error::Builder)?;
RecipientAddress::Transparent(to) => {
if memo.is_some() {
Err(Error::MemoForbidden)
} else {
builder
.add_transparent_output(&to, value)
.map_err(Error::Builder)
}
}
}?;
let consensus_branch_id = BranchId::for_height(params, height);
let (tx, tx_metadata) = builder
@ -329,12 +356,7 @@ where
// add the sapling output to shield the funds
builder
.add_sapling_output(
Some(ovk),
z_address.clone(),
amount_to_shield,
Some(memo.clone()),
)
.add_sapling_output(Some(ovk), z_address.clone(), amount_to_shield, memo.clone())
.map_err(Error::Builder)?;
let consensus_branch_id = BranchId::for_height(params, latest_anchor);

View File

@ -8,6 +8,9 @@
// Temporary until we have addressed all Result<T, ()> cases.
#![allow(clippy::result_unit_err)]
#[macro_use]
extern crate log;
pub mod address;
pub mod data_api;
mod decrypt;

View File

@ -149,6 +149,10 @@ impl<P: consensus::Parameters> WalletDb<P> {
"INSERT INTO utxos (address, prevout_txid, prevout_idx, script, value_zat, height)
VALUES (:address, :prevout_txid, :prevout_idx, :script, :value_zat, :height)"
)?,
#[cfg(feature = "transparent-inputs")]
stmt_delete_utxos: self.conn.prepare(
"DELETE FROM utxos WHERE address = :address AND height > :above_height"
)?,
stmt_insert_received_note: self.conn.prepare(
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, memo, nf, is_change)
VALUES (:tx, :output_index, :account, :diversifier, :value, :rcm, :memo, :nf, :is_change)",
@ -313,6 +317,8 @@ pub struct DataConnStmtCache<'a, P> {
#[cfg(feature = "transparent-inputs")]
stmt_insert_received_transparent_utxo: Statement<'a>,
#[cfg(feature = "transparent-inputs")]
stmt_delete_utxos: Statement<'a>,
stmt_insert_received_note: Statement<'a>,
stmt_update_received_note: Statement<'a>,
stmt_select_received_note: Statement<'a>,

View File

@ -605,11 +605,13 @@ pub fn get_unspent_transparent_utxos<P: consensus::Parameters>(
anchor_height: BlockHeight,
) -> Result<Vec<WalletTransparentOutput>, SqliteClientError> {
let mut stmt_blocks = wdb.conn.prepare(
"SELECT address, prevout_txid, prevout_idx, script, value_zat, height
FROM utxos
WHERE address = ?
AND height <= ?
AND spent_in_tx IS NULL",
"SELECT u.address, u.prevout_txid, u.prevout_idx, u.script, u.value_zat, u.height, tx.block as block
FROM utxos u
LEFT OUTER JOIN transactions tx
ON tx.id_tx = u.spent_in_tx
WHERE u.address = ?
AND u.height <= ?
AND block IS NULL",
)?;
let addr_str = address.encode(&wdb.params);
@ -789,6 +791,22 @@ pub fn put_received_transparent_utxo<'a, P: consensus::Parameters>(
Ok(UtxoId(stmts.wallet_db.conn.last_insert_rowid()))
}
#[cfg(feature = "transparent-inputs")]
pub fn delete_utxos_above<'a, P: consensus::Parameters>(
stmts: &mut DataConnStmtCache<'a, P>,
taddr: &TransparentAddress,
height: BlockHeight,
) -> Result<usize, SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(&":address", &taddr.encode(&stmts.wallet_db.params)),
(&":above_height", &u32::from(height)),
];
let rows = stmts.stmt_delete_utxos.execute_named(&sql_args)?;
Ok(rows)
}
// Assumptions:
// - A transaction will not contain more than 2^63 shielded outputs.
// - A note value will never exceed 2^63 zatoshis.

View File

@ -110,7 +110,7 @@ impl<P: consensus::Parameters> SaplingOutput<P> {
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress,
value: Amount,
memo: Option<MemoBytes>,
memo: MemoBytes,
) -> Result<Self, Error> {
Self::new_internal(params, height, rng, ovk, to, value, memo)
}
@ -122,7 +122,7 @@ impl<P: consensus::Parameters> SaplingOutput<P> {
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress,
value: Amount,
memo: Option<MemoBytes>,
memo: MemoBytes,
) -> Result<Self, Error> {
let g_d = to.g_d().ok_or(Error::InvalidAddress)?;
if value.is_negative() {
@ -142,7 +142,7 @@ impl<P: consensus::Parameters> SaplingOutput<P> {
ovk,
to,
note,
memo: memo.unwrap_or_else(MemoBytes::empty),
memo,
_params: PhantomData::default(),
})
}
@ -521,7 +521,7 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
ovk: Option<OutgoingViewingKey>,
to: PaymentAddress,
value: Amount,
memo: Option<MemoBytes>,
memo: MemoBytes,
) -> Result<(), Error> {
let output = SaplingOutput::new_internal(
&self.params,
@ -645,7 +645,12 @@ impl<'a, P: consensus::Parameters, R: RngCore> Builder<'a, P, R> {
return Err(Error::NoChangeAddress);
};
self.add_sapling_output(Some(change_address.0), change_address.1, change, None)?;
self.add_sapling_output(
Some(change_address.0),
change_address.1,
change,
MemoBytes::empty(),
)?;
}
//
@ -965,6 +970,7 @@ mod tests {
use crate::{
consensus::{self, Parameters, H0, TEST_NETWORK},
legacy::TransparentAddress,
memo::MemoBytes,
merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{prover::mock::MockTxProver, Node, Rseed},
transaction::components::{amount::Amount, amount::DEFAULT_FEE},
@ -985,7 +991,12 @@ mod tests {
let mut builder = Builder::new(TEST_NETWORK, H0);
assert_eq!(
builder.add_sapling_output(Some(ovk), to, Amount::from_i64(-1).unwrap(), None),
builder.add_sapling_output(
Some(ovk),
to,
Amount::from_i64(-1).unwrap(),
MemoBytes::empty()
),
Err(Error::InvalidAmount)
);
}
@ -1104,7 +1115,12 @@ mod tests {
{
let mut builder = Builder::new(TEST_NETWORK, H0);
builder
.add_sapling_output(ovk, to.clone(), Amount::from_u64(50000).unwrap(), None)
.add_sapling_output(
ovk,
to.clone(),
Amount::from_u64(50000).unwrap(),
MemoBytes::empty(),
)
.unwrap();
assert_eq!(
builder.build(consensus::BranchId::Sapling, &MockTxProver),
@ -1153,7 +1169,12 @@ mod tests {
)
.unwrap();
builder
.add_sapling_output(ovk, to.clone(), Amount::from_u64(30000).unwrap(), None)
.add_sapling_output(
ovk,
to.clone(),
Amount::from_u64(30000).unwrap(),
MemoBytes::empty(),
)
.unwrap();
builder
.add_transparent_output(
@ -1194,7 +1215,12 @@ mod tests {
.add_sapling_spend(extsk, *to.diversifier(), note2, witness2.path().unwrap())
.unwrap();
builder
.add_sapling_output(ovk, to, Amount::from_u64(30000).unwrap(), None)
.add_sapling_output(
ovk,
to,
Amount::from_u64(30000).unwrap(),
MemoBytes::empty(),
)
.unwrap();
builder
.add_transparent_output(