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:
parent
b88ee47e36
commit
8828276361
|
@ -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" }
|
||||||
|
|
|
@ -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."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue