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

View File

@ -59,6 +59,12 @@ pub enum Error<NoteId> {
/// The wallet attempted a sapling-only operation at a block /// The wallet attempted a sapling-only operation at a block
/// height when Sapling was not yet active. /// height when Sapling was not yet active.
SaplingNotActive, 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 { impl ChainInvalid {
@ -99,6 +105,8 @@ impl<N: fmt::Display> fmt::Display for Error<N> {
Error::Builder(e) => write!(f, "{:?}", e), Error::Builder(e) => write!(f, "{:?}", e),
Error::Protobuf(e) => write!(f, "{}", e), Error::Protobuf(e) => write!(f, "{}", e),
Error::SaplingNotActive => write!(f, "Could not determine Sapling upgrade activation height."), 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 std::fmt::Debug;
use zcash_primitives::{ use zcash_primitives::{
consensus::{self, BranchId, NetworkUpgrade}, consensus::{self, BranchId, NetworkUpgrade},
memo::MemoBytes, memo::MemoBytes,
@ -40,6 +39,8 @@ where
P: consensus::Parameters, P: consensus::Parameters,
D: WalletWrite<Error = E>, D: WalletWrite<Error = E>,
{ {
debug!("decrypt_and_store: {:?}", tx);
// Fetch the ExtendedFullViewingKeys we are tracking // Fetch the ExtendedFullViewingKeys we are tracking
let extfvks = data.get_extended_full_viewing_keys()?; let extfvks = data.get_extended_full_viewing_keys()?;
@ -54,16 +55,32 @@ where
.ok_or(Error::SaplingNotActive)?; .ok_or(Error::SaplingNotActive)?;
let outputs = decrypt_transaction(params, height, tx, &extfvks); let outputs = decrypt_transaction(params, height, tx, &extfvks);
if outputs.is_empty() { if !outputs.is_empty() {
Ok(())
} else {
data.store_received_tx(&ReceivedTransaction { data.store_received_tx(&ReceivedTransaction {
tx, tx,
outputs: &outputs, 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)] #[allow(clippy::needless_doctest_main)]
@ -224,12 +241,22 @@ where
match to { match to {
RecipientAddress::Shielded(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) => {
RecipientAddress::Transparent(to) => builder.add_transparent_output(&to, value), if memo.is_some() {
} Err(Error::MemoForbidden)
.map_err(Error::Builder)?; } else {
builder
.add_transparent_output(&to, value)
.map_err(Error::Builder)
}
}
}?;
let consensus_branch_id = BranchId::for_height(params, height); let consensus_branch_id = BranchId::for_height(params, height);
let (tx, tx_metadata) = builder let (tx, tx_metadata) = builder
@ -329,12 +356,7 @@ where
// add the sapling output to shield the funds // add the sapling output to shield the funds
builder builder
.add_sapling_output( .add_sapling_output(Some(ovk), z_address.clone(), amount_to_shield, memo.clone())
Some(ovk),
z_address.clone(),
amount_to_shield,
Some(memo.clone()),
)
.map_err(Error::Builder)?; .map_err(Error::Builder)?;
let consensus_branch_id = BranchId::for_height(params, latest_anchor); 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. // Temporary until we have addressed all Result<T, ()> cases.
#![allow(clippy::result_unit_err)] #![allow(clippy::result_unit_err)]
#[macro_use]
extern crate log;
pub mod address; pub mod address;
pub mod data_api; pub mod data_api;
mod decrypt; 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) "INSERT INTO utxos (address, prevout_txid, prevout_idx, script, value_zat, height)
VALUES (: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( stmt_insert_received_note: self.conn.prepare(
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, memo, nf, is_change) "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)", 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")] #[cfg(feature = "transparent-inputs")]
stmt_insert_received_transparent_utxo: Statement<'a>, stmt_insert_received_transparent_utxo: Statement<'a>,
#[cfg(feature = "transparent-inputs")]
stmt_delete_utxos: Statement<'a>,
stmt_insert_received_note: Statement<'a>, stmt_insert_received_note: Statement<'a>,
stmt_update_received_note: Statement<'a>, stmt_update_received_note: Statement<'a>,
stmt_select_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, anchor_height: BlockHeight,
) -> Result<Vec<WalletTransparentOutput>, SqliteClientError> { ) -> Result<Vec<WalletTransparentOutput>, SqliteClientError> {
let mut stmt_blocks = wdb.conn.prepare( let mut stmt_blocks = wdb.conn.prepare(
"SELECT address, prevout_txid, prevout_idx, script, value_zat, height "SELECT u.address, u.prevout_txid, u.prevout_idx, u.script, u.value_zat, u.height, tx.block as block
FROM utxos FROM utxos u
WHERE address = ? LEFT OUTER JOIN transactions tx
AND height <= ? ON tx.id_tx = u.spent_in_tx
AND spent_in_tx IS NULL", WHERE u.address = ?
AND u.height <= ?
AND block IS NULL",
)?; )?;
let addr_str = address.encode(&wdb.params); 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())) 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: // Assumptions:
// - A transaction will not contain more than 2^63 shielded outputs. // - A transaction will not contain more than 2^63 shielded outputs.
// - A note value will never exceed 2^63 zatoshis. // - A note value will never exceed 2^63 zatoshis.

View File

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