Merge pull request #677 from nuttycom/wallet/decrypt_marks_utxos_spent

Mark our utxos spent when we detect them as inputs to a transaction.
This commit is contained in:
str4d 2022-10-14 21:12:21 +01:00 committed by GitHub
commit 5301b9c9d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 124 additions and 67 deletions

View File

@ -14,8 +14,7 @@ and this library adheres to Rust's notion of
to facilitate the automatic shielding of transparent funds received
by the wallet.
- A `zcash_client_backend::wallet::WalletTransparentOutput` type
has been added under the `transparent-inputs` feature flag in support
of autoshielding functionality.
in support of `transparent-inputs` functionality.
- A new `data_api::wallet::spend` method has been added, which is
intended to supersede the `data_api::wallet::create_spend_to_address`
method. This new method now constructs transactions via interpretation

View File

@ -10,9 +10,6 @@ use zcash_primitives::{
},
};
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::{legacy::keys::IncomingViewingKey, sapling::keys::OutgoingViewingKey};
use crate::{
address::RecipientAddress,
data_api::{
@ -25,6 +22,9 @@ use crate::{
zip321::{Payment, TransactionRequest},
};
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::{legacy::keys::IncomingViewingKey, sapling::keys::OutgoingViewingKey};
/// 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<N, E, P, D>(
@ -50,14 +50,10 @@ where
.or_else(|| params.activation_height(NetworkUpgrade::Sapling))
.ok_or(Error::SaplingNotActive)?;
let sapling_outputs = decrypt_transaction(params, height, tx, &ufvks);
if !(sapling_outputs.is_empty() && tx.transparent_bundle().iter().all(|b| b.vout.is_empty())) {
data.store_decrypted_tx(&DecryptedTransaction {
tx,
sapling_outputs: &sapling_outputs,
})?;
}
data.store_decrypted_tx(&DecryptedTransaction {
tx,
sapling_outputs: &decrypt_transaction(params, height, tx, &ufvks),
})?;
Ok(())
}
@ -479,7 +475,7 @@ where
let utxos = wallet_db.get_unspent_transparent_outputs(&taddr, latest_anchor)?;
let total_amount = utxos
.iter()
.map(|utxo| utxo.txout.value)
.map(|utxo| utxo.txout().value)
.sum::<Option<Amount>>()
.ok_or_else(|| E::from(Error::InvalidAmount))?;
@ -498,7 +494,7 @@ where
.unwrap();
for utxo in &utxos {
builder
.add_transparent_input(secret_key, utxo.outpoint.clone(), utxo.txout.clone())
.add_transparent_input(secret_key, utxo.outpoint().clone(), utxo.txout().clone())
.map_err(Error::Builder)?;
}
@ -526,6 +522,6 @@ where
memo: Some(memo.clone()),
}],
fee_amount: fee,
utxos_spent: utxos.iter().map(|utxo| utxo.outpoint.clone()).collect(),
utxos_spent: utxos.iter().map(|utxo| utxo.outpoint().clone()).collect(),
})
}

View File

@ -30,15 +30,44 @@ pub struct WalletTx<N> {
pub shielded_outputs: Vec<WalletShieldedOutput<N>>,
}
#[derive(Debug, Clone)]
pub struct WalletTransparentOutput {
pub outpoint: OutPoint,
pub txout: TxOut,
pub height: BlockHeight,
outpoint: OutPoint,
txout: TxOut,
height: BlockHeight,
recipient_address: TransparentAddress,
}
impl WalletTransparentOutput {
pub fn address(&self) -> TransparentAddress {
self.txout.script_pubkey.address().unwrap()
pub fn from_parts(
outpoint: OutPoint,
txout: TxOut,
height: BlockHeight,
) -> Option<WalletTransparentOutput> {
txout
.recipient_address()
.map(|recipient_address| WalletTransparentOutput {
outpoint,
txout,
height,
recipient_address,
})
}
pub fn outpoint(&self) -> &OutPoint {
&self.outpoint
}
pub fn txout(&self) -> &TxOut {
&self.txout
}
pub fn height(&self) -> BlockHeight {
self.height
}
pub fn recipient_address(&self) -> &TransparentAddress {
&self.recipient_address
}
}

View File

@ -538,7 +538,6 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
&mut self,
d_tx: &DecryptedTransaction,
) -> Result<Self::TxRef, Self::Error> {
let nullifiers = self.wallet_db.get_all_nullifiers()?;
self.transactionally(|up| {
let tx_ref = wallet::put_tx_data(up, d_tx.tx, None, None)?;
@ -579,8 +578,15 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
}
}
// If any of the utxos spent in the transaction are ours, mark them as spent.
#[cfg(feature = "transparent-inputs")]
for txin in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vin.iter()) {
wallet::mark_transparent_utxo_spent(up, tx_ref, &txin.prevout)?;
}
// If we have some transparent outputs:
if !d_tx.tx.transparent_bundle().iter().any(|b| b.vout.is_empty()) {
let nullifiers = self.wallet_db.get_all_nullifiers()?;
// If the transaction contains shielded spends from our wallet, we will store z->t
// transactions we observe in the same way they would be stored by
// create_spend_to_address.
@ -590,16 +596,17 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
.any(|input| *nf == input.nullifier)
) {
for (output_index, txout) in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vout.iter()).enumerate() {
let recipient = Recipient::Transparent(txout.script_pubkey.address().unwrap());
wallet::put_sent_output(
up,
*account_id,
tx_ref,
output_index,
&recipient,
txout.value,
None
)?;
if let Some(address) = txout.recipient_address() {
wallet::put_sent_output(
up,
*account_id,
tx_ref,
output_index,
&Recipient::Transparent(address),
txout.value,
None
)?;
}
}
}
}

View File

@ -513,12 +513,12 @@ impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
self.stmt_insert_received_transparent_utxo
.query_row(
named_params![
":address": &output.address().encode(&self.wallet_db.params),
":prevout_txid": &output.outpoint.hash().to_vec(),
":prevout_idx": &output.outpoint.n(),
":script": &output.txout.script_pubkey.0,
":value_zat": &i64::from(output.txout.value),
":height": &u32::from(output.height),
":address": &output.recipient_address().encode(&self.wallet_db.params),
":prevout_txid": &output.outpoint().hash().to_vec(),
":prevout_idx": &output.outpoint().n(),
":script": &output.txout().script_pubkey.0,
":value_zat": &i64::from(output.txout().value),
":height": &u32::from(output.height()),
],
|row| {
let id = row.get(0)?;
@ -540,12 +540,12 @@ impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
self.stmt_update_received_transparent_utxo
.query_row(
named_params![
":prevout_txid": &output.outpoint.hash().to_vec(),
":prevout_idx": &output.outpoint.n(),
":address": &output.address().encode(&self.wallet_db.params),
":script": &output.txout.script_pubkey.0,
":value_zat": &i64::from(output.txout.value),
":height": &u32::from(output.height),
":prevout_txid": &output.outpoint().hash().to_vec(),
":prevout_idx": &output.outpoint().n(),
":address": &output.recipient_address().encode(&self.wallet_db.params),
":script": &output.txout().script_pubkey.0,
":value_zat": &i64::from(output.txout().value),
":height": &u32::from(output.height()),
],
|row| {
let id = row.get(0)?;

View File

@ -933,7 +933,9 @@ pub(crate) fn get_unspent_transparent_outputs<P: consensus::Parameters>(
let addr_str = address.encode(&wdb.params);
let rows = stmt_blocks.query_map(params![addr_str, u32::from(max_height)], |row| {
let mut utxos = Vec::<WalletTransparentOutput>::new();
let mut rows = stmt_blocks.query(params![addr_str, u32::from(max_height)])?;
while let Some(row) = rows.next()? {
let txid: Vec<u8> = row.get(0)?;
let mut txid_bytes = [0u8; 32];
txid_bytes.copy_from_slice(&txid);
@ -943,21 +945,24 @@ pub(crate) fn get_unspent_transparent_outputs<P: consensus::Parameters>(
let value = Amount::from_i64(row.get(3)?).unwrap();
let height: u32 = row.get(4)?;
Ok(WalletTransparentOutput {
outpoint: OutPoint::new(txid_bytes, index),
txout: TxOut {
let output = WalletTransparentOutput::from_parts(
OutPoint::new(txid_bytes, index),
TxOut {
value,
script_pubkey,
},
height: BlockHeight::from(height),
})
})?;
BlockHeight::from(height),
)
.ok_or_else(|| {
SqliteClientError::CorruptedData(
"Txout script_pubkey value did not correspond to a P2PKH or P2SH address"
.to_string(),
)
})?;
let mut utxos = Vec::<WalletTransparentOutput>::new();
for utxo in rows {
utxos.push(utxo.unwrap())
utxos.push(output);
}
Ok(utxos)
}
@ -1261,22 +1266,31 @@ mod tests {
let uaddr = db_data.get_current_address(account_id).unwrap().unwrap();
let taddr = uaddr.transparent().unwrap();
let mut utxo = WalletTransparentOutput {
outpoint: OutPoint::new([1u8; 32], 1),
txout: TxOut {
let utxo = WalletTransparentOutput::from_parts(
OutPoint::new([1u8; 32], 1),
TxOut {
value: Amount::from_u64(100000).unwrap(),
script_pubkey: taddr.script(),
},
height: BlockHeight::from_u32(12345),
};
BlockHeight::from_u32(12345),
)
.unwrap();
let res0 = super::put_received_transparent_utxo(&mut ops, &utxo);
assert!(matches!(res0, Ok(_)));
// Change something about the UTXO and upsert; we should get back
// Change the mined height of the UTXO and upsert; we should get back
// the same utxoid
utxo.height = BlockHeight::from_u32(34567);
let res1 = super::put_received_transparent_utxo(&mut ops, &utxo);
let utxo2 = WalletTransparentOutput::from_parts(
OutPoint::new([1u8; 32], 1),
TxOut {
value: Amount::from_u64(100000).unwrap(),
script_pubkey: taddr.script(),
},
BlockHeight::from_u32(34567),
)
.unwrap();
let res1 = super::put_received_transparent_utxo(&mut ops, &utxo2);
assert!(matches!(res1, Ok(id) if id == res0.unwrap()));
assert!(matches!(
@ -1296,7 +1310,7 @@ mod tests {
),
Ok(utxos) if {
utxos.len() == 1 &&
utxos.iter().any(|rutxo| rutxo.height == utxo.height)
utxos.iter().any(|rutxo| rutxo.height() == utxo2.height())
}
));
@ -1310,7 +1324,7 @@ mod tests {
)
.unwrap();
let res2 = super::put_received_transparent_utxo(&mut ops, &utxo);
let res2 = super::put_received_transparent_utxo(&mut ops, &utxo2);
assert!(matches!(res2, Err(_)));
}
}

View File

@ -39,6 +39,7 @@ and this library adheres to Rust's notion of
- `JSDescription::net_value`
- Added in `zcash_primitives::transaction::components::transparent`
- `Bundle::value_balance`
- `TxOut::recipient_address`
- Implementations of `memuse::DynamicUsage` for the following types:
- `zcash_primitives::block::BlockHash`
- `zcash_primitives::consensus`:
@ -73,6 +74,12 @@ and this library adheres to Rust's notion of
- `try_sapling_note_decryption` and `try_sapling_compact_note_decryption` now
take `&PreparedIncomingViewingKey` instead of `&SaplingIvk`.
### Removed
- `zcash_primitives::legacy::Script::address` This method was not generally
safe to use on arbitrary scripts, only on script_pubkey values. Its
functionality is now available via
`zcash_primitives::transaction::components::transparent::TxOut::recipient_address`
## [0.7.0] - 2022-06-24
### Changed
- Bumped dependencies to `equihash 0.2`, `orchard 0.2`.

View File

@ -43,7 +43,7 @@ impl Script {
}
/// Returns the address that this Script contains, if any.
pub fn address(&self) -> Option<TransparentAddress> {
pub(crate) fn address(&self) -> Option<TransparentAddress> {
if self.0.len() == 25
&& self.0[0..3] == [OpCode::Dup as u8, OpCode::Hash160 as u8, 0x14]
&& self.0[23..25] == [OpCode::EqualVerify as u8, OpCode::CheckSig as u8]

View File

@ -5,7 +5,7 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::fmt::Debug;
use std::io::{self, Read, Write};
use crate::legacy::Script;
use crate::legacy::{Script, TransparentAddress};
use super::amount::{Amount, BalanceError};
@ -182,6 +182,11 @@ impl TxOut {
writer.write_all(&self.value.to_i64_le_bytes())?;
self.script_pubkey.write(&mut writer)
}
/// Returns the address to which the TxOut was sent, if this is a valid P2SH or P2PKH output.
pub fn recipient_address(&self) -> Option<TransparentAddress> {
self.script_pubkey.address()
}
}
#[cfg(any(test, feature = "test-dependencies"))]