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