From cff457ff15af74ca87a2922054413f7fa21afc69 Mon Sep 17 00:00:00 2001 From: Francisco Gindre Date: Tue, 22 Dec 2020 11:10:13 -0300 Subject: [PATCH] PoC Auto-Shielding Add retrieval of transparent UTXOs to WalletRead Co-authored-by: Kris Nuttycombe Co-authored-by: Kevin Gorham --- zcash_client_backend/Cargo.toml | 4 + zcash_client_backend/src/data_api.rs | 34 +++++-- zcash_client_backend/src/data_api/error.rs | 2 + zcash_client_backend/src/data_api/wallet.rs | 100 +++++++++++++++++- zcash_client_backend/src/encoding.rs | 57 +++++++++++ zcash_client_backend/src/keys.rs | 20 +++- zcash_client_backend/src/wallet.rs | 15 ++- zcash_client_sqlite/src/error.rs | 6 +- zcash_client_sqlite/src/lib.rs | 95 +++++++++++++----- zcash_client_sqlite/src/wallet.rs | 106 ++++++++++++++++++-- zcash_client_sqlite/src/wallet/init.rs | 15 +++ zcash_client_sqlite/src/wallet/transact.rs | 4 +- 12 files changed, 415 insertions(+), 43 deletions(-) diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index da0518aa7..1d7f8e9f2 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -26,6 +26,9 @@ percent-encoding = "2.1.0" proptest = { version = "0.10.1", optional = true } protobuf = "2.20" rand_core = "0.5.1" +ripemd160 = { version = "0.9.1", optional = true } +secp256k1 = { version = "0.20", optional = true } +sha2 = "0.9" subtle = "2.2.3" time = "0.2" zcash_note_encryption = { version = "0.0", path = "../components/zcash_note_encryption" } @@ -43,6 +46,7 @@ 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"] [badges] diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 166fd80cb..2d8a33eb1 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -7,10 +7,14 @@ use std::fmt::Debug; use zcash_primitives::{ block::BlockHash, consensus::BlockHeight, + legacy::TransparentAddress, memo::{Memo, MemoBytes}, merkle_tree::{CommitmentTree, IncrementalWitness}, sapling::{Node, Nullifier, PaymentAddress}, - transaction::{components::Amount, Transaction, TxId}, + transaction::{ + components::{Amount, OutPoint}, + Transaction, TxId, + }, zip32::ExtendedFullViewingKey, }; @@ -19,7 +23,7 @@ use crate::{ data_api::wallet::ANCHOR_OFFSET, decrypt::DecryptedOutput, proto::compact_formats::CompactBlock, - wallet::{AccountId, SpendableNote, WalletTx}, + wallet::{AccountId, SpendableNote, WalletTransparentOutput, WalletTx}, }; pub mod chain; @@ -160,7 +164,7 @@ pub trait WalletRead { fn get_nullifiers(&self) -> Result, Self::Error>; /// Return all spendable notes. - fn get_spendable_notes( + fn get_spendable_sapling_notes( &self, account: AccountId, anchor_height: BlockHeight, @@ -168,12 +172,18 @@ pub trait WalletRead { /// Returns a list of spendable notes sufficient to cover the specified /// target value, if possible. - fn select_spendable_notes( + fn select_spendable_sapling_notes( &self, account: AccountId, target_value: Amount, anchor_height: BlockHeight, ) -> Result, Self::Error>; + + fn get_spendable_transparent_utxos( + &self, + address: &TransparentAddress, + anchor_height: BlockHeight, + ) -> Result, Self::Error>; } /// The subset of information that is relevant to this wallet that has been @@ -215,6 +225,7 @@ pub struct SentTransaction<'a> { pub recipient_address: &'a RecipientAddress, pub value: Amount, pub memo: Option, + pub utxos_spent: Vec, } /// This trait encapsulates the write capabilities required to update stored @@ -274,6 +285,7 @@ pub mod testing { use zcash_primitives::{ block::BlockHash, consensus::BlockHeight, + legacy::TransparentAddress, memo::Memo, merkle_tree::{CommitmentTree, IncrementalWitness}, sapling::{Node, Nullifier, PaymentAddress}, @@ -283,7 +295,7 @@ pub mod testing { use crate::{ proto::compact_formats::CompactBlock, - wallet::{AccountId, SpendableNote}, + wallet::{AccountId, SpendableNote, WalletTransparentOutput}, }; use super::{ @@ -380,7 +392,7 @@ pub mod testing { Ok(Vec::new()) } - fn get_spendable_notes( + fn get_spendable_sapling_notes( &self, _account: AccountId, _anchor_height: BlockHeight, @@ -388,7 +400,7 @@ pub mod testing { Ok(Vec::new()) } - fn select_spendable_notes( + fn select_spendable_sapling_notes( &self, _account: AccountId, _target_value: Amount, @@ -396,6 +408,14 @@ pub mod testing { ) -> Result, Self::Error> { Ok(Vec::new()) } + + fn get_spendable_transparent_utxos( + &self, + _address: &TransparentAddress, + _anchor_height: BlockHeight, + ) -> Result, Self::Error> { + Ok(Vec::new()) + } } impl WalletWrite for MockWalletDb { diff --git a/zcash_client_backend/src/data_api/error.rs b/zcash_client_backend/src/data_api/error.rs index 1968af8f9..086daee40 100644 --- a/zcash_client_backend/src/data_api/error.rs +++ b/zcash_client_backend/src/data_api/error.rs @@ -25,6 +25,8 @@ pub enum ChainInvalid { #[derive(Debug)] pub enum Error { /// Unable to create a new spend because the wallet balance is not sufficient. + /// The first argument is the amount available, the second is the amount needed + /// to construct a valid transaction. InsufficientBalance(Amount, Amount), /// Chain validation detected an error in the block at the specified block height. diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index 82a9a9a9b..be5760614 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -20,6 +20,12 @@ use crate::{ wallet::{AccountId, OvkPolicy}, }; +#[cfg(feature = "transparent-inputs")] +use zcash_primitives::{legacy::Script, transaction::components::TxOut}; + +#[cfg(feature = "transparent-inputs")] +use crate::keys::derive_transparent_address_from_secret_key; + pub const ANCHOR_OFFSET: u32 = 10; /// Scans a [`Transaction`] for any information that can be decrypted by the accounts in @@ -184,7 +190,8 @@ where .and_then(|x| x.ok_or_else(|| Error::ScanRequired.into()))?; let target_value = value + DEFAULT_FEE; - let spendable_notes = wallet_db.select_spendable_notes(account, target_value, anchor_height)?; + let spendable_notes = + wallet_db.select_spendable_sapling_notes(account, target_value, anchor_height)?; // Confirm we were able to select sufficient value let selected_value = spendable_notes.iter().map(|n| n.note_value).sum(); @@ -254,5 +261,96 @@ where recipient_address: to, value, memo, + utxos_spent: vec![], + }) +} + +#[cfg(feature = "transparent-inputs")] +pub fn shield_funds( + wallet_db: &mut D, + params: &P, + prover: impl TxProver, + account: AccountId, + sk: &secp256k1::SecretKey, + extsk: &ExtendedSpendingKey, + memo: &MemoBytes, +) -> Result +where + E: From>, + P: consensus::Parameters, + R: Copy + Debug, + D: WalletWrite, +{ + let (latest_scanned_height, latest_anchor) = wallet_db + .get_target_and_anchor_heights() + .and_then(|x| x.ok_or_else(|| Error::ScanRequired.into()))?; + + // derive the corresponding t-address + let taddr = derive_transparent_address_from_secret_key(*sk); + + // derive own shielded address from the provided extended spending key + let z_address = extsk.default_address().unwrap().1; + + let exfvk = ExtendedFullViewingKey::from(extsk); + + let ovk = exfvk.fvk.ovk; + + // get UTXOs from DB + let utxos = wallet_db.get_spendable_transparent_utxos(&taddr, latest_anchor)?; + let total_amount = utxos.iter().map(|utxo| utxo.value).sum::(); + + let fee = DEFAULT_FEE; + if fee >= total_amount { + return Err(E::from(Error::InsufficientBalance(total_amount, fee))); + } + + let amount_to_shield = total_amount - fee; + + let mut builder = Builder::new(params.clone(), latest_scanned_height); + + for utxo in &utxos { + let coin = TxOut { + value: utxo.value, + script_pubkey: Script { + 0: utxo.script.clone(), + }, + }; + + builder + .add_transparent_input(*sk, utxo.outpoint.clone(), coin) + .map_err(Error::Builder)?; + } + + // there are no sapling notes so we set the change manually + builder.send_change_to(ovk, z_address.clone()); + + // add the sapling output to shield the funds + builder + .add_sapling_output( + Some(ovk), + z_address.clone(), + amount_to_shield, + Some(memo.clone()), + ) + .map_err(Error::Builder)?; + + let consensus_branch_id = BranchId::for_height(params, latest_anchor); + + let (tx, tx_metadata) = builder + .build(consensus_branch_id, &prover) + .map_err(Error::Builder)?; + let output_index = tx_metadata.output_index(0).expect( + "No sapling note was created in autoshielding transaction. This is a programming error.", + ); + + wallet_db.store_sent_tx(&SentTransaction { + tx: &tx, + created: time::OffsetDateTime::now_utc(), + output_index, + account, + recipient_address: &RecipientAddress::Shielded(z_address), + value: amount_to_shield, + memo: Some(memo.clone()), + utxos_spent: utxos.iter().map(|utxo| utxo.outpoint.clone()).collect(), }) } diff --git a/zcash_client_backend/src/encoding.rs b/zcash_client_backend/src/encoding.rs index 827b99bf0..18cda7292 100644 --- a/zcash_client_backend/src/encoding.rs +++ b/zcash_client_backend/src/encoding.rs @@ -8,8 +8,10 @@ use bech32::{self, Error, FromBase32, ToBase32, Variant}; use bs58::{self, decode::Error as Bs58Error}; use std::convert::TryInto; +use std::fmt; use std::io::{self, Write}; use zcash_primitives::{ + consensus, legacy::TransparentAddress, sapling::PaymentAddress, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, @@ -36,6 +38,61 @@ where } } +pub trait AddressCodec

+where + Self: std::marker::Sized, +{ + type Error; + + fn encode(&self, params: &P) -> String; + fn decode(params: &P, address: &str) -> Result; +} + +#[derive(Debug)] +pub enum TransparentCodecError { + UnsupportedAddressType(String), + Base58(Bs58Error), +} + +impl fmt::Display for TransparentCodecError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match &self { + TransparentCodecError::UnsupportedAddressType(s) => write!( + f, + "Could not recognize {} as a supported p2sh or p2pkh address.", + s + ), + TransparentCodecError::Base58(e) => write!(f, "{}", e), + } + } +} + +impl std::error::Error for TransparentCodecError {} + +impl AddressCodec

for TransparentAddress { + type Error = TransparentCodecError; + + fn encode(&self, params: &P) -> String { + encode_transparent_address( + ¶ms.b58_pubkey_address_prefix(), + ¶ms.b58_script_address_prefix(), + self, + ) + } + + fn decode(params: &P, address: &str) -> Result { + decode_transparent_address( + ¶ms.b58_pubkey_address_prefix(), + ¶ms.b58_script_address_prefix(), + address, + ) + .map_err(TransparentCodecError::Base58) + .and_then(|opt| { + opt.ok_or_else(|| TransparentCodecError::UnsupportedAddressType(address.to_string())) + }) + } +} + /// Writes an [`ExtendedSpendingKey`] as a Bech32-encoded string. /// /// # Examples diff --git a/zcash_client_backend/src/keys.rs b/zcash_client_backend/src/keys.rs index c2fd72b34..c0de03693 100644 --- a/zcash_client_backend/src/keys.rs +++ b/zcash_client_backend/src/keys.rs @@ -1,6 +1,14 @@ //! Helper functions for managing light client key material. +#![cfg(feature = "transparent-inputs")] -use zcash_primitives::zip32::{ChildIndex, ExtendedSpendingKey}; +use zcash_primitives::{ + legacy::TransparentAddress, + zip32::{ChildIndex, ExtendedSpendingKey}, +}; + +use secp256k1::{key::PublicKey, Secp256k1}; + +use sha2::{Digest, Sha256}; /// Derives the ZIP 32 [`ExtendedSpendingKey`] for a given coin type and account from the /// given seed. @@ -33,6 +41,16 @@ pub fn spending_key(seed: &[u8], coin_type: u32, account: u32) -> ExtendedSpendi ) } +pub fn derive_transparent_address_from_secret_key( + secret_key: secp256k1::key::SecretKey, +) -> TransparentAddress { + let secp = Secp256k1::new(); + let pk = PublicKey::from_secret_key(&secp, &secret_key); + let mut hash160 = ripemd160::Ripemd160::new(); + hash160.update(Sha256::digest(&pk.serialize()[..].to_vec())); + TransparentAddress::PublicKey(*hash160.finalize().as_ref()) +} + #[cfg(test)] mod tests { use super::spending_key; diff --git a/zcash_client_backend/src/wallet.rs b/zcash_client_backend/src/wallet.rs index 220aaf6e3..39e3d9d2c 100644 --- a/zcash_client_backend/src/wallet.rs +++ b/zcash_client_backend/src/wallet.rs @@ -4,11 +4,16 @@ use subtle::{Choice, ConditionallySelectable}; use zcash_primitives::{ + consensus::BlockHeight, + legacy::TransparentAddress, merkle_tree::IncrementalWitness, sapling::{ keys::OutgoingViewingKey, Diversifier, Node, Note, Nullifier, PaymentAddress, Rseed, }, - transaction::{components::Amount, TxId}, + transaction::{ + components::{Amount, OutPoint}, + TxId, + }, }; /// A type-safe wrapper for account identifiers. @@ -39,6 +44,14 @@ pub struct WalletTx { pub shielded_outputs: Vec>, } +pub struct WalletTransparentOutput { + pub address: TransparentAddress, + pub outpoint: OutPoint, + pub script: Vec, + pub value: Amount, + pub height: BlockHeight, +} + /// A subset of a [`SpendDescription`] relevant to wallets and light clients. /// /// [`SpendDescription`]: zcash_primitives::transaction::components::SpendDescription diff --git a/zcash_client_sqlite/src/error.rs b/zcash_client_sqlite/src/error.rs index e83c20e5d..6c285fb4c 100644 --- a/zcash_client_sqlite/src/error.rs +++ b/zcash_client_sqlite/src/error.rs @@ -3,7 +3,7 @@ use std::error; use std::fmt; -use zcash_client_backend::data_api; +use zcash_client_backend::{data_api, encoding::TransparentCodecError}; use crate::NoteId; @@ -32,6 +32,9 @@ pub enum SqliteClientError { /// Base58 decoding error Base58(bs58::decode::Error), + /// Base58 decoding error + TransparentAddress(TransparentCodecError), + /// Wrapper for rusqlite errors. DbError(rusqlite::Error), @@ -68,6 +71,7 @@ impl fmt::Display for SqliteClientError { SqliteClientError::InvalidNoteId => write!(f, "The note ID associated with an inserted witness must correspond to a received note."), SqliteClientError::Bech32(e) => write!(f, "{}", e), SqliteClientError::Base58(e) => write!(f, "{}", e), + SqliteClientError::TransparentAddress(e) => write!(f, "{}", e), SqliteClientError::TableNotEmpty => write!(f, "Table is not empty"), SqliteClientError::DbError(e) => write!(f, "{}", e), SqliteClientError::Io(e) => write!(f, "{}", e), diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 0d9047574..c9ba7cad8 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -41,6 +41,7 @@ use rusqlite::{Connection, Statement, NO_PARAMS}; use zcash_primitives::{ block::BlockHash, consensus::{self, BlockHeight}, + legacy::TransparentAddress, memo::Memo, merkle_tree::{CommitmentTree, IncrementalWitness}, sapling::{Node, Nullifier, PaymentAddress}, @@ -54,7 +55,7 @@ use zcash_client_backend::{ }, encoding::encode_payment_address, proto::compact_formats::CompactBlock, - wallet::{AccountId, SpendableNote}, + wallet::{AccountId, SpendableNote, WalletTransparentOutput}, }; use crate::error::SqliteClientError; @@ -80,6 +81,11 @@ impl fmt::Display for NoteId { } } +/// A newtype wrapper for sqlite primary key values for the utxos +/// table. +#[derive(Debug, Copy, Clone)] +pub struct UtxoId(i64); + /// A wrapper for the SQLite connection to the wallet database. pub struct WalletDb

{ conn: Connection, @@ -91,7 +97,9 @@ impl WalletDb

{ pub fn for_path>(path: F, params: P) -> Result { Connection::open(path).map(move |conn| WalletDb { conn, params }) } +} +impl WalletDb

{ /// Given a wallet database connection, obtain a handle for the write operations /// for that database. This operation may eagerly initialize and cache sqlite /// prepared statements that are used in write operations. @@ -122,9 +130,18 @@ impl WalletDb

{ stmt_select_tx_ref: self.conn.prepare( "SELECT id_tx FROM transactions WHERE txid = ?", )?, - stmt_mark_recived_note_spent: self.conn.prepare( + stmt_mark_sapling_note_spent: self.conn.prepare( "UPDATE received_notes SET spent = ? WHERE nf = ?" )?, + stmt_mark_transparent_utxo_spent: self.conn.prepare( + "UPDATE utxos SET spent_in_tx = :spent_in_tx + WHERE prevout_txid = :prevout_txid + AND prevout_idx = :prevout_idx" + )?, + stmt_insert_received_transparent_utxo: self.conn.prepare( + "INSERT INTO utxos (address, prevout_txid, prevout_idx, script, value_zat, height) + VALUES (:address, :prevout_txid, :prevout_idx, :script, :value_zat, :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)", @@ -176,25 +193,25 @@ impl WalletRead for WalletDb

{ type TxRef = i64; fn block_height_extrema(&self) -> Result, Self::Error> { - wallet::block_height_extrema(self).map_err(SqliteClientError::from) + wallet::block_height_extrema(&self).map_err(SqliteClientError::from) } fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { - wallet::get_block_hash(self, block_height).map_err(SqliteClientError::from) + wallet::get_block_hash(&self, block_height).map_err(SqliteClientError::from) } fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { - wallet::get_tx_height(self, txid).map_err(SqliteClientError::from) + wallet::get_tx_height(&self, txid).map_err(SqliteClientError::from) } fn get_extended_full_viewing_keys( &self, ) -> Result, Self::Error> { - wallet::get_extended_full_viewing_keys(self) + wallet::get_extended_full_viewing_keys(&self) } fn get_address(&self, account: AccountId) -> Result, Self::Error> { - wallet::get_address(self, account) + wallet::get_address(&self, account) } fn is_valid_account_extfvk( @@ -202,7 +219,7 @@ impl WalletRead for WalletDb

{ account: AccountId, extfvk: &ExtendedFullViewingKey, ) -> Result { - wallet::is_valid_account_extfvk(self, account, extfvk) + wallet::is_valid_account_extfvk(&self, account, extfvk) } fn get_balance_at( @@ -210,7 +227,7 @@ impl WalletRead for WalletDb

{ account: AccountId, anchor_height: BlockHeight, ) -> Result { - wallet::get_balance_at(self, account, anchor_height) + wallet::get_balance_at(&self, account, anchor_height) } fn get_memo(&self, id_note: Self::NoteRef) -> Result { @@ -224,7 +241,7 @@ impl WalletRead for WalletDb

{ &self, block_height: BlockHeight, ) -> Result>, Self::Error> { - wallet::get_commitment_tree(self, block_height) + wallet::get_commitment_tree(&self, block_height) } #[allow(clippy::type_complexity)] @@ -232,28 +249,41 @@ impl WalletRead for WalletDb

{ &self, block_height: BlockHeight, ) -> Result)>, Self::Error> { - wallet::get_witnesses(self, block_height) + wallet::get_witnesses(&self, block_height) } fn get_nullifiers(&self) -> Result, Self::Error> { - wallet::get_nullifiers(self) + wallet::get_nullifiers(&self) } - fn get_spendable_notes( + fn get_spendable_sapling_notes( &self, account: AccountId, anchor_height: BlockHeight, ) -> Result, Self::Error> { - wallet::transact::get_spendable_notes(self, account, anchor_height) + wallet::transact::get_spendable_sapling_notes(&self, account, anchor_height) } - fn select_spendable_notes( + fn select_spendable_sapling_notes( &self, account: AccountId, target_value: Amount, anchor_height: BlockHeight, ) -> Result, Self::Error> { - wallet::transact::select_spendable_notes(self, account, target_value, anchor_height) + wallet::transact::select_spendable_sapling_notes( + &self, + account, + target_value, + anchor_height, + ) + } + + fn get_spendable_transparent_utxos( + &self, + address: &TransparentAddress, + anchor_height: BlockHeight, + ) -> Result, Self::Error> { + wallet::get_spendable_transparent_utxos(&self, address, anchor_height) } } @@ -275,8 +305,10 @@ pub struct DataConnStmtCache<'a, P> { stmt_update_tx_data: Statement<'a>, stmt_select_tx_ref: Statement<'a>, - stmt_mark_recived_note_spent: Statement<'a>, + stmt_mark_sapling_note_spent: Statement<'a>, + stmt_mark_transparent_utxo_spent: Statement<'a>, + stmt_insert_received_transparent_utxo: Statement<'a>, stmt_insert_received_note: Statement<'a>, stmt_update_received_note: Statement<'a>, stmt_select_received_note: Statement<'a>, @@ -355,22 +387,32 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> { self.wallet_db.get_nullifiers() } - fn get_spendable_notes( + fn get_spendable_sapling_notes( &self, account: AccountId, anchor_height: BlockHeight, ) -> Result, Self::Error> { - self.wallet_db.get_spendable_notes(account, anchor_height) + self.wallet_db + .get_spendable_sapling_notes(account, anchor_height) } - fn select_spendable_notes( + fn select_spendable_sapling_notes( &self, account: AccountId, target_value: Amount, anchor_height: BlockHeight, ) -> Result, Self::Error> { self.wallet_db - .select_spendable_notes(account, target_value, anchor_height) + .select_spendable_sapling_notes(account, target_value, anchor_height) + } + + fn get_spendable_transparent_utxos( + &self, + address: &TransparentAddress, + anchor_height: BlockHeight, + ) -> Result, Self::Error> { + self.wallet_db + .get_spendable_transparent_utxos(address, anchor_height) } } @@ -426,9 +468,12 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> { // Mark notes as spent and remove them from the scanning cache for spend in &tx.shielded_spends { - wallet::mark_spent(up, tx_row, &spend.nf)?; + wallet::mark_sapling_note_spent(up, tx_row, &spend.nf)?; } + //TODO + //wallet::mark_transparent_utxo_spent(up, tx_ref, &utxo.outpoint)?; + for output in &tx.shielded_outputs { let received_note_id = wallet::put_received_note(up, output, tx_row)?; @@ -490,7 +535,11 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> { // Assumes that create_spend_to_address() will never be called in parallel, which is a // reasonable assumption for a light client such as a mobile phone. for spend in &sent_tx.tx.shielded_spends { - wallet::mark_spent(up, tx_ref, &spend.nullifier)?; + wallet::mark_sapling_note_spent(up, tx_ref, &spend.nullifier)?; + } + + for utxo_outpoint in &sent_tx.utxos_spent { + wallet::mark_transparent_utxo_spent(up, tx_ref, &utxo_outpoint)?; } wallet::insert_sent_note( diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 9fbc41185..483f80e7f 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -15,10 +15,14 @@ use std::convert::TryFrom; use zcash_primitives::{ block::BlockHash, consensus::{self, BlockHeight, NetworkUpgrade}, + legacy::TransparentAddress, memo::{Memo, MemoBytes}, merkle_tree::{CommitmentTree, IncrementalWitness}, sapling::{Node, Note, Nullifier, PaymentAddress}, - transaction::{components::Amount, Transaction, TxId}, + transaction::{ + components::{Amount, OutPoint}, + Transaction, TxId, + }, zip32::ExtendedFullViewingKey, }; @@ -27,13 +31,13 @@ use zcash_client_backend::{ data_api::error::Error, encoding::{ decode_extended_full_viewing_key, decode_payment_address, encode_extended_full_viewing_key, - encode_payment_address, + encode_payment_address, AddressCodec, }, - wallet::{AccountId, WalletShieldedOutput, WalletTx}, + wallet::{AccountId, WalletShieldedOutput, WalletTransparentOutput, WalletTx}, DecryptedOutput, }; -use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDb}; +use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, UtxoId, WalletDb}; pub mod init; pub mod transact; @@ -588,6 +592,57 @@ pub fn get_nullifiers

( Ok(res) } +pub fn get_spendable_transparent_utxos( + wdb: &WalletDb

, + address: &TransparentAddress, + anchor_height: BlockHeight, +) -> Result, 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", + )?; + + let addr_str = address.encode(&wdb.params); + + let rows = stmt_blocks.query_map(params![addr_str, u32::from(anchor_height)], |row| { + let addr: String = row.get(0)?; + let address = TransparentAddress::decode(&wdb.params, &addr).map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + addr.len(), + rusqlite::types::Type::Text, + Box::new(e), + ) + })?; + + let id: Vec = row.get(1)?; + + let mut txid_bytes = [0u8; 32]; + txid_bytes.copy_from_slice(&id); + let index: i32 = row.get(2)?; + let script: Vec = row.get(3)?; + let value: i64 = row.get(4)?; + let height: u32 = row.get(5)?; + + Ok(WalletTransparentOutput { + address, + outpoint: OutPoint::new(txid_bytes, index as u32), + script, + value: Amount::from_i64(value).unwrap(), + height: BlockHeight::from(height), + }) + })?; + + let mut utxos = Vec::::new(); + + for utxo in rows { + utxos.push(utxo.unwrap()) + } + Ok(utxos) +} + /// Inserts information about a scanned block into the database. pub fn insert_block<'a, P>( stmts: &mut DataConnStmtCache<'a, P>, @@ -676,18 +731,56 @@ pub fn put_tx_data<'a, P>( /// /// Marking a note spent in this fashion does NOT imply that the /// spending transaction has been mined. -pub fn mark_spent<'a, P>( +pub fn mark_sapling_note_spent<'a, P>( stmts: &mut DataConnStmtCache<'a, P>, tx_ref: i64, nf: &Nullifier, ) -> Result<(), SqliteClientError> { stmts - .stmt_mark_recived_note_spent + .stmt_mark_sapling_note_spent .execute(&[tx_ref.to_sql()?, nf.0.to_sql()?])?; Ok(()) } /// Records the specified shielded output as having been received. +pub fn mark_transparent_utxo_spent<'a, P>( + stmts: &mut DataConnStmtCache<'a, P>, + tx_ref: i64, + outpoint: &OutPoint, +) -> Result<(), SqliteClientError> { + let sql_args: &[(&str, &dyn ToSql)] = &[ + (&":spent_in_tx", &tx_ref), + (&":prevout_txid", &outpoint.hash().to_vec()), + (&":prevout_idx", &outpoint.n()), + ]; + + stmts + .stmt_mark_transparent_utxo_spent + .execute_named(&sql_args)?; + + Ok(()) +} + +pub fn put_received_transparent_utxo<'a, P: consensus::Parameters>( + stmts: &mut DataConnStmtCache<'a, P>, + output: &WalletTransparentOutput, +) -> Result { + let sql_args: &[(&str, &dyn ToSql)] = &[ + (&":address", &output.address.encode(&stmts.wallet_db.params)), + (&":prevout_txid", &output.outpoint.hash().to_vec()), + (&":prevout_idx", &output.outpoint.n()), + (&":script", &output.script), + (&":value_zat", &i64::from(output.value)), + (&":height", &u32::from(output.height)), + ]; + + stmts + .stmt_insert_received_transparent_utxo + .execute_named(&sql_args)?; + + Ok(UtxoId(stmts.wallet_db.conn.last_insert_rowid())) +} + // Assumptions: // - A transaction will not contain more than 2^63 shielded outputs. // - A note value will never exceed 2^63 zatoshis. @@ -847,7 +940,6 @@ pub fn insert_sent_note<'a, P: consensus::Parameters>( Ok(()) } - #[cfg(test)] mod tests { use tempfile::NamedTempFile; diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index f21f53182..572e54b2b 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -106,6 +106,21 @@ pub fn init_wallet_db

(wdb: &WalletDb

) -> Result<(), rusqlite::Error> { )", NO_PARAMS, )?; + wdb.conn.execute( + "CREATE TABLE IF NOT EXISTS utxos ( + id_utxo INTEGER PRIMARY KEY, + address TEXT NOT NULL, + prevout_txid BLOB NOT NULL, + prevout_idx INTEGER NOT NULL, + script BLOB NOT NULL, + value_zat INTEGER NOT NULL, + height INTEGER NOT NULL, + spent_in_tx INTEGER, + FOREIGN KEY (spent_in_tx) REFERENCES transactions(id_tx), + CONSTRAINT tx_outpoint UNIQUE (prevout_txid, prevout_idx) + )", + NO_PARAMS, + )?; Ok(()) } diff --git a/zcash_client_sqlite/src/wallet/transact.rs b/zcash_client_sqlite/src/wallet/transact.rs index 5fccb430f..35ed92f0c 100644 --- a/zcash_client_sqlite/src/wallet/transact.rs +++ b/zcash_client_sqlite/src/wallet/transact.rs @@ -59,7 +59,7 @@ fn to_spendable_note(row: &Row) -> Result { }) } -pub fn get_spendable_notes

( +pub fn get_spendable_sapling_notes

( wdb: &WalletDb

, account: AccountId, anchor_height: BlockHeight, @@ -87,7 +87,7 @@ pub fn get_spendable_notes

( notes.collect::>() } -pub fn select_spendable_notes

( +pub fn select_spendable_sapling_notes

( wdb: &WalletDb

, account: AccountId, target_value: Amount,