diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index ce03b8e3b..0e94b70de 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -31,12 +31,17 @@ and this library adheres to Rust's notion of - `zcash_client_backend::address`: - `RecipientAddress::Unified` - `zcash_client_backend::data_api`: + - `PoolType` + - `Recipient` + - `SentTransactionOutput` - `WalletRead::get_unified_full_viewing_keys` - `WalletRead::get_current_address` - `WalletRead::get_all_nullifiers` - `WalletWrite::create_account` - `WalletWrite::remove_unmined_tx` (behind the `unstable` feature flag). - `WalletWrite::get_next_available_address` +- `zcash_client_backend::decrypt`: + - `TransferType` - `zcash_client_backend::proto`: - `actions` field on `compact_formats::CompactTx` - `compact_formats::CompactOrchardAction` @@ -44,7 +49,10 @@ and this library adheres to Rust's notion of - `TransactionRequest::new` for constructing a request from `Vec`. - `TransactionRequest::payments` for accessing the `Payments` that make up a request. -- `zcash_client_backend::encoding::KeyError` +- `zcash_client_backend::encoding` + - `KeyError` + - `AddressCodec` implementations for `sapling::PaymentAddress` and + `UnifiedAddress` - New experimental APIs that should be considered unstable, and are likely to be modified and/or moved to a different module in a future release: diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index a2eea7f09..8f2040bc0 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -11,15 +11,16 @@ use secrecy::SecretVec; use zcash_primitives::{ block::BlockHash, consensus::BlockHeight, + legacy::TransparentAddress, memo::{Memo, MemoBytes}, merkle_tree::{CommitmentTree, IncrementalWitness}, - sapling::{Node, Nullifier}, + sapling::{Node, Nullifier, PaymentAddress}, transaction::{components::Amount, Transaction, TxId}, zip32::{AccountId, ExtendedFullViewingKey}, }; use crate::{ - address::{RecipientAddress, UnifiedAddress}, + address::UnifiedAddress, decrypt::DecryptedOutput, keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, proto::compact_formats::CompactBlock, @@ -27,10 +28,7 @@ use crate::{ }; #[cfg(feature = "transparent-inputs")] -use { - crate::wallet::WalletTransparentOutput, - zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint}, -}; +use {crate::wallet::WalletTransparentOutput, zcash_primitives::transaction::components::OutPoint}; pub mod chain; pub mod error; @@ -248,13 +246,34 @@ pub struct SentTransaction<'a> { pub tx: &'a Transaction, pub created: time::OffsetDateTime, pub account: AccountId, - pub outputs: Vec>, + pub outputs: Vec, pub fee_amount: Amount, #[cfg(feature = "transparent-inputs")] pub utxos_spent: Vec, } -pub struct SentTransactionOutput<'a> { +/// A value pool to which the wallet supports sending transaction outputs. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PoolType { + /// The transparent value pool + Transparent, + /// The Sapling value pool + Sapling, +} + +/// A type that represents the recipient of a transaction output; a recipient address (and, for +/// unified addresses, the pool to which the payment is sent) in the case of outgoing output, or an +/// internal account ID and the pool to which funds were sent in the case of a wallet-internal +/// output. +#[derive(Debug, Clone)] +pub enum Recipient { + Transparent(TransparentAddress), + Sapling(PaymentAddress), + Unified(UnifiedAddress, PoolType), + InternalAccount(AccountId, PoolType), +} + +pub struct SentTransactionOutput { /// The index within the transaction that contains the recipient output. /// /// - If `recipient_address` is a Sapling address, this is an index into the Sapling @@ -262,8 +281,12 @@ pub struct SentTransactionOutput<'a> { /// - If `recipient_address` is a transparent address, this is an index into the /// transparent outputs of the transaction. pub output_index: usize, - pub recipient_address: &'a RecipientAddress, + /// The recipient address of the transaction, or the account + /// id for wallet-internal transactions. + pub recipient: Recipient, + /// The value of the newly created output pub value: Amount, + /// The memo that was attached to the output, if any pub memo: Option, } diff --git a/zcash_client_backend/src/data_api/wallet.rs b/zcash_client_backend/src/data_api/wallet.rs index 675a9ffad..bfcb81ee2 100644 --- a/zcash_client_backend/src/data_api/wallet.rs +++ b/zcash_client_backend/src/data_api/wallet.rs @@ -23,7 +23,8 @@ use { use crate::{ address::RecipientAddress, data_api::{ - error::Error, DecryptedTransaction, SentTransaction, SentTransactionOutput, WalletWrite, + error::Error, DecryptedTransaction, PoolType, Recipient, SentTransaction, + SentTransactionOutput, WalletWrite, }, decrypt_transaction, wallet::OvkPolicy, @@ -373,14 +374,21 @@ where let (tx, tx_metadata) = builder.build(&prover).map_err(Error::Builder)?; let sent_outputs = request.payments().iter().enumerate().map(|(i, payment)| { - let idx = match &payment.recipient_address { + let (output_index, recipient) = match &payment.recipient_address { // Sapling outputs are shuffled, so we need to look up where the output ended up. - // TODO: When we add Orchard support, we will need to trial-decrypt to find them. - RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => - tx_metadata.output_index(i).expect("An output should exist in the transaction for each shielded payment."), + RecipientAddress::Shielded(addr) => { + let idx = tx_metadata.output_index(i).expect("An output should exist in the transaction for each shielded payment."); + (idx, Recipient::Sapling(addr.clone())) + } + RecipientAddress::Unified(addr) => { + // TODO: When we add Orchard support, we will need to trial-decrypt to find them, + // and return the appropriate pool type. + let idx = tx_metadata.output_index(i).expect("An output should exist in the transaction for each shielded payment."); + (idx, Recipient::Unified(addr.clone(), PoolType::Sapling)) + } RecipientAddress::Transparent(addr) => { let script = addr.script(); - tx.transparent_bundle() + let idx = tx.transparent_bundle() .and_then(|b| { b.vout .iter() @@ -388,13 +396,15 @@ where .find(|(_, tx_out)| tx_out.script_pubkey == script) }) .map(|(index, _)| index) - .expect("An output should exist in the transaction for each transparent payment.") + .expect("An output should exist in the transaction for each transparent payment."); + + (idx, Recipient::Transparent(*addr)) } }; SentTransactionOutput { - output_index: idx, - recipient_address: &payment.recipient_address, + output_index, + recipient, value: payment.amount, memo: payment.memo.clone() } @@ -512,12 +522,7 @@ where // add the sapling output to shield the funds builder - .add_sapling_output( - Some(ovk), - shielding_address.clone(), - amount_to_shield, - memo.clone(), - ) + .add_sapling_output(Some(ovk), shielding_address, amount_to_shield, memo.clone()) .map_err(Error::Builder)?; let (tx, tx_metadata) = builder.build(&prover).map_err(Error::Builder)?; @@ -531,8 +536,8 @@ where account, outputs: vec![SentTransactionOutput { output_index, - recipient_address: &RecipientAddress::Shielded(shielding_address), value: amount_to_shield, + recipient: Recipient::InternalAccount(account, PoolType::Sapling), memo: Some(memo.clone()), }], fee_amount: fee, diff --git a/zcash_client_backend/src/decrypt.rs b/zcash_client_backend/src/decrypt.rs index 64086c166..7f25f14d0 100644 --- a/zcash_client_backend/src/decrypt.rs +++ b/zcash_client_backend/src/decrypt.rs @@ -10,11 +10,25 @@ use zcash_primitives::{ Note, PaymentAddress, }, transaction::Transaction, - zip32::AccountId, + zip32::{AccountId, Scope}, }; use crate::keys::UnifiedFullViewingKey; +/// An enumeration of the possible relationships a TXO can have to the wallet. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TransferType { + /// The output was received on one of the wallet's external addresses via decryption using the + /// associated incoming viewing key, or at one of the wallet's transparent addresses. + Incoming, + /// The output was received on one of the wallet's internal-only shielded addresses via trial + /// decryption using one of the wallet's internal incoming viewing keys. + WalletInternal, + /// The output was decrypted using one of the wallet's outgoing viewing keys, or was created + /// in a transaction constructed by this wallet. + Outgoing, +} + /// A decrypted shielded output. pub struct DecryptedOutput { /// The index of the output within [`shielded_outputs`]. @@ -33,7 +47,7 @@ pub struct DecryptedOutput { /// this is a logical output of the transaction. /// /// [`OutgoingViewingKey`]: zcash_primitives::keys::OutgoingViewingKey - pub outgoing: bool, + pub transfer_type: TransferType, } /// Scans a [`Transaction`] for any information that can be decrypted by the set of @@ -44,37 +58,52 @@ pub fn decrypt_transaction( tx: &Transaction, ufvks: &HashMap, ) -> Vec { - let mut decrypted = vec![]; + tx.sapling_bundle() + .iter() + .flat_map(|bundle| { + ufvks + .iter() + .flat_map(move |(account, ufvk)| { + ufvk.sapling().into_iter().map(|dfvk| (*account, dfvk)) + }) + .flat_map(move |(account, dfvk)| { + let ivk_external = + PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::External)); + let ivk_internal = + PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal)); + let ovk = dfvk.fvk().ovk; - if let Some(bundle) = tx.sapling_bundle() { - for (account, ufvk) in ufvks.iter() { - if let Some(dfvk) = ufvk.sapling() { - let ivk = PreparedIncomingViewingKey::new(&dfvk.fvk().vk.ivk()); - let ovk = dfvk.fvk().ovk; - - for (index, output) in bundle.shielded_outputs.iter().enumerate() { - let ((note, to, memo), outgoing) = - match try_sapling_note_decryption(params, height, &ivk, output) { - Some(ret) => (ret, false), - None => match try_sapling_output_recovery(params, height, &ovk, output) - { - Some(ret) => (ret, true), - None => continue, - }, - }; - - decrypted.push(DecryptedOutput { - index, - note, - account: *account, - to, - memo, - outgoing, - }) - } - } - } - } - - decrypted + bundle + .shielded_outputs + .iter() + .enumerate() + .flat_map(move |(index, output)| { + try_sapling_note_decryption(params, height, &ivk_external, output) + .map(|ret| (ret, TransferType::Incoming)) + .or_else(|| { + try_sapling_note_decryption( + params, + height, + &ivk_internal, + output, + ) + .map(|ret| (ret, TransferType::WalletInternal)) + }) + .or_else(|| { + try_sapling_output_recovery(params, height, &ovk, output) + .map(|ret| (ret, TransferType::Outgoing)) + }) + .into_iter() + .map(move |((note, to, memo), transfer_type)| DecryptedOutput { + index, + note, + account, + to, + memo, + transfer_type, + }) + }) + }) + }) + .collect() } diff --git a/zcash_client_backend/src/encoding.rs b/zcash_client_backend/src/encoding.rs index b63a4aff8..6350c47e6 100644 --- a/zcash_client_backend/src/encoding.rs +++ b/zcash_client_backend/src/encoding.rs @@ -5,14 +5,16 @@ //! //! [constants]: zcash_primitives::constants +use crate::address::UnifiedAddress; use bech32::{self, Error, FromBase32, ToBase32, Variant}; use bs58::{self, decode::Error as Bs58Error}; use std::fmt; use std::io::{self, Write}; +use zcash_address::unified::{self, Encoding}; use zcash_primitives::{ consensus, legacy::TransparentAddress, - sapling::PaymentAddress, + sapling, zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, }; @@ -132,6 +134,41 @@ impl AddressCodec

for TransparentAddress { } } +impl AddressCodec

for sapling::PaymentAddress { + type Error = Bech32DecodeError; + + fn encode(&self, params: &P) -> String { + encode_payment_address(params.hrp_sapling_payment_address(), self) + } + + fn decode(params: &P, address: &str) -> Result { + decode_payment_address(params.hrp_sapling_payment_address(), address) + } +} + +impl AddressCodec

for UnifiedAddress { + type Error = String; + + fn encode(&self, params: &P) -> String { + self.encode(params) + } + + fn decode(params: &P, address: &str) -> Result { + unified::Address::decode(address) + .map_err(|e| format!("{}", e)) + .and_then(|(network, addr)| { + if params.address_network() == Some(network) { + UnifiedAddress::try_from(addr).map_err(|e| e.to_owned()) + } else { + Err(format!( + "Address {} is for a different network: {:?}", + address, network + )) + } + }) + } +} + /// Writes an [`ExtendedSpendingKey`] as a Bech32-encoded string. /// /// # Examples @@ -232,16 +269,18 @@ pub fn decode_extended_full_viewing_key( /// ); /// ``` /// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress -pub fn encode_payment_address(hrp: &str, addr: &PaymentAddress) -> String { +pub fn encode_payment_address(hrp: &str, addr: &sapling::PaymentAddress) -> String { bech32_encode(hrp, |w| w.write_all(&addr.to_bytes())) } /// Writes a [`PaymentAddress`] as a Bech32-encoded string /// using the human-readable prefix values defined in the specified /// network parameters. +/// +/// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress pub fn encode_payment_address_p( params: &P, - addr: &PaymentAddress, + addr: &sapling::PaymentAddress, ) -> String { encode_payment_address(params.hrp_sapling_payment_address(), addr) } @@ -259,7 +298,7 @@ pub fn encode_payment_address_p( /// encoding::decode_payment_address, /// }; /// use zcash_primitives::{ -/// constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS, +/// consensus::{TEST_NETWORK, Parameters}, /// sapling::{Diversifier, PaymentAddress}, /// }; /// @@ -276,14 +315,17 @@ pub fn encode_payment_address_p( /// /// assert_eq!( /// decode_payment_address( -/// HRP_SAPLING_PAYMENT_ADDRESS, +/// TEST_NETWORK.hrp_sapling_payment_address(), /// "ztestsapling1qqqqqqqqqqqqqqqqqqcguyvaw2vjk4sdyeg0lc970u659lvhqq7t0np6hlup5lusxle75ss7jnk", /// ), /// Ok(pa), /// ); /// ``` /// [`PaymentAddress`]: zcash_primitives::sapling::PaymentAddress -pub fn decode_payment_address(hrp: &str, s: &str) -> Result { +pub fn decode_payment_address( + hrp: &str, + s: &str, +) -> Result { bech32_decode(hrp, s, |data| { if data.len() != 43 { return None; @@ -291,7 +333,7 @@ pub fn decode_payment_address(hrp: &str, s: &str) -> Result Result Result( /// /// ``` /// use zcash_primitives::{ -/// constants::testnet::{B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX}, +/// consensus::{TEST_NETWORK, Parameters}, /// }; /// use zcash_client_backend::{ /// encoding::decode_transparent_address, @@ -378,8 +420,8 @@ pub fn encode_transparent_address_p( /// /// assert_eq!( /// decode_transparent_address( -/// &B58_PUBKEY_ADDRESS_PREFIX, -/// &B58_SCRIPT_ADDRESS_PREFIX, +/// &TEST_NETWORK.b58_pubkey_address_prefix(), +/// &TEST_NETWORK.b58_script_address_prefix(), /// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma", /// ), /// Ok(Some(TransparentAddress::PublicKey([0; 20]))), @@ -387,8 +429,8 @@ pub fn encode_transparent_address_p( /// /// assert_eq!( /// decode_transparent_address( -/// &B58_PUBKEY_ADDRESS_PREFIX, -/// &B58_SCRIPT_ADDRESS_PREFIX, +/// &TEST_NETWORK.b58_pubkey_address_prefix(), +/// &TEST_NETWORK.b58_script_address_prefix(), /// "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2", /// ), /// Ok(Some(TransparentAddress::Script([0; 20]))), diff --git a/zcash_client_backend/src/lib.rs b/zcash_client_backend/src/lib.rs index 2bcf755e9..1eb5163c7 100644 --- a/zcash_client_backend/src/lib.rs +++ b/zcash_client_backend/src/lib.rs @@ -19,4 +19,4 @@ pub mod wallet; pub mod welding_rig; pub mod zip321; -pub use decrypt::{decrypt_transaction, DecryptedOutput}; +pub use decrypt::{decrypt_transaction, DecryptedOutput, TransferType}; diff --git a/zcash_client_sqlite/CHANGELOG.md b/zcash_client_sqlite/CHANGELOG.md index e104a3447..8a72b9ea8 100644 --- a/zcash_client_sqlite/CHANGELOG.md +++ b/zcash_client_sqlite/CHANGELOG.md @@ -76,11 +76,6 @@ and this library adheres to Rust's notion of - `zcash_client_sqlite::wallet`: - `get_spendable_notes` to `get_spendable_sapling_notes`. - `select_spendable_notes` to `select_spendable_sapling_notes`. -- Altered the arguments to `zcash_client_sqlite::wallet::put_sent_note` - to take the components of a `DecryptedOutput` value to allow this - method to be used in contexts where a transaction has just been - constructed, rather than only in the case that a transaction has - been decrypted after being retrieved from the network. - `zcash_client_sqlite::wallet::init_wallet_db` has been modified to take the wallet seed as an argument so that it can correctly perform migrations that require re-deriving key material. In particular for @@ -89,12 +84,15 @@ and this library adheres to Rust's notion of migration process. ### Removed -- `zcash_client_sqlite::wallet`: - - `get_extended_full_viewing_keys` (use - `zcash_client_backend::data_api::WalletRead::get_unified_full_viewing_keys` - instead). - - `delete_utxos_above` (use - `zcash_client_backend::data_api::WalletWrite::rewind_to_height` instead) +- The following functions have been removed from the public interface of + `zcash_client_sqlite::wallet`. Prefer methods defined on + `zcash_client_backend::data_api::{WalletRead, WalletWrite}` instead. + - `get_extended_full_viewing_keys` (use `WalletRead::get_unified_full_viewing_keys` instead). + - `insert_sent_note` (use `WalletWrite::store_sent_tx` instead) + - `insert_sent_utxo` (use `WalletWrite::store_sent_tx` instead) + - `put_sent_note` (use `WalletWrite::store_decrypted_tx` instead) + - `put_sent_utxo` (use `WalletWrite::store_decrypted_tx` instead) + - `delete_utxos_above` (use `WalletWrite::rewind_to_height` instead) - `zcash_client_sqlite::with_blocks` (use `zcash_client_backend::data_api::BlockSource::with_blocks` instead) @@ -111,7 +109,6 @@ and this library adheres to Rust's notion of `zcash_client_backend::data_api` traits mentioned above instead. - Deprecated in `zcash_client_sqlite::wallet`: - `get_address` - - `get_extended_full_viewing_keys` - `is_valid_account_extfvk` - `get_balance` - `get_balance_at` @@ -131,10 +128,6 @@ and this library adheres to Rust's notion of - `insert_witness` - `prune_witnesses` - `update_expired_notes` - - `put_sent_note` - - `put_sent_utxo` - - `insert_sent_note` - - `insert_sent_utxo` - `get_address` - Deprecated in `zcash_client_sqlite::wallet::transact`: - `get_spendable_sapling_notes` diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 9583cf65a..19c5e29b3 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -53,13 +53,15 @@ use zcash_primitives::{ }; use zcash_client_backend::{ - address::{RecipientAddress, UnifiedAddress}, + address::UnifiedAddress, data_api::{ - BlockSource, DecryptedTransaction, PrunedBlock, SentTransaction, WalletRead, WalletWrite, + BlockSource, DecryptedTransaction, PoolType, PrunedBlock, Recipient, SentTransaction, + WalletRead, WalletWrite, }, keys::{UnifiedFullViewingKey, UnifiedSpendingKey}, proto::compact_formats::CompactBlock, wallet::SpendableNote, + TransferType, }; use crate::error::SqliteClientError; @@ -529,29 +531,38 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> { let mut spending_account_id: Option = None; for output in d_tx.sapling_outputs { - if output.outgoing { - wallet::put_sent_note( - up, - tx_ref, - output.index, - output.account, - &output.to, - Amount::from_u64(output.note.value) - .map_err(|_| SqliteClientError::CorruptedData("Note value invalid.".to_string()))?, - Some(&output.memo), - )?; - } else { - match spending_account_id { - Some(id) => - if id != output.account { - panic!("Unable to determine a unique account identifier for z->t spend."); - } - None => { - spending_account_id = Some(output.account); - } - } + match output.transfer_type { + TransferType::Outgoing | TransferType::WalletInternal => { + let recipient = if output.transfer_type == TransferType::Outgoing { + Recipient::Sapling(output.to.clone()) + } else { + Recipient::InternalAccount(output.account, PoolType::Sapling) + }; - wallet::put_received_note(up, output, tx_ref)?; + wallet::put_sent_output( + up, + output.account, + tx_ref, + output.index, + &recipient, + Amount::from_u64(output.note.value).map_err(|_| + SqliteClientError::CorruptedData("Note value is not a valid Zcash amount.".to_string()))?, + Some(&output.memo), + )?; + } + TransferType::Incoming => { + match spending_account_id { + Some(id) => + if id != output.account { + panic!("Unable to determine a unique account identifier for z->t spend."); + } + None => { + spending_account_id = Some(output.account); + } + } + + wallet::put_received_note(up, output, tx_ref)?; + } } } @@ -566,13 +577,15 @@ 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() { - wallet::put_sent_utxo( + let recipient = Recipient::Transparent(txout.script_pubkey.address().unwrap()); + wallet::put_sent_output( up, + *account_id, tx_ref, output_index, - *account_id, - &txout.script_pubkey.address().unwrap(), + &recipient, txout.value, + None )?; } } @@ -611,36 +624,7 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> { } for output in &sent_tx.outputs { - match output.recipient_address { - // TODO: Store the entire UA, not just the Sapling component. - // This will require more info about the output index. - RecipientAddress::Unified(ua) => wallet::insert_sent_note( - up, - tx_ref, - output.output_index, - sent_tx.account, - ua.sapling().expect("TODO: Add Orchard support"), - output.value, - output.memo.as_ref(), - )?, - RecipientAddress::Shielded(addr) => wallet::insert_sent_note( - up, - tx_ref, - output.output_index, - sent_tx.account, - addr, - output.value, - output.memo.as_ref(), - )?, - RecipientAddress::Transparent(addr) => wallet::insert_sent_utxo( - up, - tx_ref, - output.output_index, - sent_tx.account, - addr, - output.value, - )?, - } + wallet::insert_sent_output(up, tx_ref, sent_tx.account, output)?; } // Return the row number of the transaction, so the caller can fetch it for sending. diff --git a/zcash_client_sqlite/src/prepared.rs b/zcash_client_sqlite/src/prepared.rs index 67de34861..741829106 100644 --- a/zcash_client_sqlite/src/prepared.rs +++ b/zcash_client_sqlite/src/prepared.rs @@ -7,7 +7,7 @@ //! - Build the statement in [`DataConnStmtCache::new`]. //! - Add a crate-private helper method to `DataConnStmtCache` for running the statement. -use rusqlite::{params, Statement, ToSql}; +use rusqlite::{named_params, params, Statement, ToSql}; use zcash_primitives::{ block::BlockHash, consensus::{self, BlockHeight}, @@ -18,15 +18,18 @@ use zcash_primitives::{ zip32::{AccountId, DiversifierIndex}, }; -use zcash_client_backend::address::UnifiedAddress; +use zcash_client_backend::{ + address::UnifiedAddress, + data_api::{PoolType, Recipient}, + encoding::AddressCodec, +}; -use crate::{error::SqliteClientError, wallet::PoolType, NoteId, WalletDb}; +use crate::{error::SqliteClientError, wallet::pool_code, NoteId, WalletDb}; #[cfg(feature = "transparent-inputs")] use { - crate::UtxoId, - rusqlite::{named_params, OptionalExtension}, - zcash_client_backend::{encoding::AddressCodec, wallet::WalletTransparentOutput}, + crate::UtxoId, rusqlite::OptionalExtension, + zcash_client_backend::wallet::WalletTransparentOutput, zcash_primitives::transaction::components::transparent::OutPoint, }; @@ -60,8 +63,8 @@ pub struct DataConnStmtCache<'a, P> { stmt_update_received_note: Statement<'a>, stmt_select_received_note: Statement<'a>, - stmt_insert_sent_note: Statement<'a>, - stmt_update_sent_note: Statement<'a>, + stmt_insert_sent_output: Statement<'a>, + stmt_update_sent_output: Statement<'a>, stmt_insert_witness: Statement<'a>, stmt_prune_witnesses: Statement<'a>, @@ -152,19 +155,24 @@ impl<'a, P> DataConnStmtCache<'a, P> { stmt_select_received_note: wallet_db.conn.prepare( "SELECT id_note FROM received_notes WHERE tx = ? AND output_index = ?" )?, - stmt_update_sent_note: wallet_db.conn.prepare( + stmt_update_sent_output: wallet_db.conn.prepare( "UPDATE sent_notes - SET from_account = :account, - address = :address, + SET from_account = :from_account, + to_address = :to_address, + to_account = :to_account, value = :value, memo = IFNULL(:memo, memo) WHERE tx = :tx AND output_pool = :output_pool AND output_index = :output_index", )?, - stmt_insert_sent_note: wallet_db.conn.prepare( - "INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value, memo) - VALUES (:tx, :output_pool, :output_index, :from_account, :address, :value, :memo)" + stmt_insert_sent_output: wallet_db.conn.prepare( + "INSERT INTO sent_notes ( + tx, output_pool, output_index, from_account, + to_address, to_account, value, memo) + VALUES ( + :tx, :output_pool, :output_index, :from_account, + :to_address, :to_account, :value, :memo)" )?, stmt_insert_witness: wallet_db.conn.prepare( "INSERT INTO sapling_witnesses (note, block, witness) @@ -346,6 +354,105 @@ impl<'a, P> DataConnStmtCache<'a, P> { } impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> { + /// Inserts a sent note into the wallet database. + /// + /// `output_index` is the index within the transaction that contains the recipient output: + /// + /// - If `to` is a Unified address, this is an index into the outputs of the transaction + /// within the bundle associated with the recipient's output pool. + /// - If `to` is a Sapling address, this is an index into the Sapling outputs of the + /// transaction. + /// - If `to` is a transparent address, this is an index into the transparent outputs of + /// the transaction. + /// - If `to` is an internal account, this is an index into the Sapling outputs of the + /// transaction. + #[allow(clippy::too_many_arguments)] + pub(crate) fn stmt_insert_sent_output( + &mut self, + tx_ref: i64, + output_index: usize, + from_account: AccountId, + to: &Recipient, + value: Amount, + memo: Option<&MemoBytes>, + ) -> Result<(), SqliteClientError> { + let (to_address, to_account, pool_type) = match to { + Recipient::Transparent(addr) => ( + Some(addr.encode(&self.wallet_db.params)), + None, + PoolType::Transparent, + ), + Recipient::Sapling(addr) => ( + Some(addr.encode(&self.wallet_db.params)), + None, + PoolType::Sapling, + ), + Recipient::Unified(addr, pool) => { + (Some(addr.encode(&self.wallet_db.params)), None, *pool) + } + Recipient::InternalAccount(id, pool) => (None, Some(u32::from(*id)), *pool), + }; + + self.stmt_insert_sent_output.execute(named_params![ + ":tx": &tx_ref, + ":output_pool": &pool_code(pool_type), + ":output_index": &i64::try_from(output_index).unwrap(), + ":from_account": &u32::from(from_account), + ":to_address": &to_address, + ":to_account": &to_account, + ":value": &i64::from(value), + ":memo": &memo.filter(|m| *m != &MemoBytes::empty()).map(|m| m.as_slice()), + ])?; + + Ok(()) + } + + /// Updates the data for the given sent note. + /// + /// Returns `false` if the transaction doesn't exist in the wallet. + #[allow(clippy::too_many_arguments)] + pub(crate) fn stmt_update_sent_output( + &mut self, + from_account: AccountId, + to: &Recipient, + value: Amount, + memo: Option<&MemoBytes>, + tx_ref: i64, + output_index: usize, + ) -> Result { + let (to_address, to_account, pool_type) = match to { + Recipient::Transparent(addr) => ( + Some(addr.encode(&self.wallet_db.params)), + None, + PoolType::Transparent, + ), + Recipient::Sapling(addr) => ( + Some(addr.encode(&self.wallet_db.params)), + None, + PoolType::Sapling, + ), + Recipient::Unified(addr, pool) => { + (Some(addr.encode(&self.wallet_db.params)), None, *pool) + } + Recipient::InternalAccount(id, pool) => (None, Some(u32::from(*id)), *pool), + }; + + match self.stmt_update_sent_output.execute(named_params![ + ":from_account": &u32::from(from_account), + ":to_address": &to_address, + ":to_account": &to_account, + ":value": &i64::from(value), + ":memo": &memo.filter(|m| *m != &MemoBytes::empty()).map(|m| m.as_slice()), + ":tx": &tx_ref, + ":output_pool": &pool_code(pool_type), + ":output_index": &i64::try_from(output_index).unwrap(), + ])? { + 0 => Ok(false), + 1 => Ok(true), + _ => unreachable!("tx_output constraint is marked as UNIQUE"), + } + } + /// Adds the given received UTXO to the datastore. /// /// Returns the database row for the newly-inserted UTXO, or an error if the UTXO @@ -528,78 +635,6 @@ impl<'a, P> DataConnStmtCache<'a, P> { .map_err(SqliteClientError::from) } - /// Inserts a sent note into the wallet database. - /// - /// `output_index` is the index within the transaction that contains the recipient output: - /// - /// - If `to` is a Sapling address, this is an index into the Sapling outputs of the - /// transaction. - /// - If `to` is a transparent address, this is an index into the transparent outputs of - /// the transaction. - #[allow(clippy::too_many_arguments)] - pub(crate) fn stmt_insert_sent_note( - &mut self, - tx_ref: i64, - pool_type: PoolType, - output_index: usize, - account: AccountId, - to_str: &str, - value: Amount, - memo: Option<&MemoBytes>, - ) -> Result<(), SqliteClientError> { - let sql_args: &[(&str, &dyn ToSql)] = &[ - (":tx", &tx_ref), - (":output_pool", &pool_type.typecode()), - (":output_index", &i64::try_from(output_index).unwrap()), - (":from_account", &u32::from(account)), - (":address", &to_str), - (":value", &i64::from(value)), - ( - ":memo", - &memo - .filter(|m| *m != &MemoBytes::empty()) - .map(|m| m.as_slice()), - ), - ]; - self.stmt_insert_sent_note.execute(sql_args)?; - Ok(()) - } - - /// Updates the data for the given sent note. - /// - /// Returns `false` if the transaction doesn't exist in the wallet. - #[allow(clippy::too_many_arguments)] - pub(crate) fn stmt_update_sent_note( - &mut self, - account: AccountId, - to_str: &str, - value: Amount, - memo: Option<&MemoBytes>, - tx_ref: i64, - pool_type: PoolType, - output_index: usize, - ) -> Result { - let sql_args: &[(&str, &dyn ToSql)] = &[ - (":account", &u32::from(account)), - (":address", &to_str), - (":value", &i64::from(value)), - ( - ":memo", - &memo - .filter(|m| *m != &MemoBytes::empty()) - .map(|m| m.as_slice()), - ), - (":tx", &tx_ref), - (":output_pool", &pool_type.typecode()), - (":output_index", &i64::try_from(output_index).unwrap()), - ]; - match self.stmt_update_sent_note.execute(sql_args)? { - 0 => Ok(false), - 1 => Ok(true), - _ => unreachable!("tx_output constraint is marked as UNIQUE"), - } - } - /// Records the incremental witness for the specified note, as of the given block /// height. /// diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 6afa1db65..70fb64814 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -27,8 +27,7 @@ use zcash_primitives::{ use zcash_client_backend::{ address::{RecipientAddress, UnifiedAddress}, - data_api::error::Error, - encoding::{encode_payment_address_p, encode_transparent_address_p}, + data_api::{error::Error, PoolType, Recipient, SentTransactionOutput}, keys::UnifiedFullViewingKey, wallet::{WalletShieldedOutput, WalletTx}, DecryptedOutput, @@ -36,8 +35,6 @@ use zcash_client_backend::{ use crate::{error::SqliteClientError, DataConnStmtCache, NoteId, WalletDb, PRUNING_HEIGHT}; -use zcash_primitives::legacy::TransparentAddress; - #[cfg(feature = "transparent-inputs")] use { crate::UtxoId, @@ -45,7 +42,7 @@ use { std::collections::HashSet, zcash_client_backend::{encoding::AddressCodec, wallet::WalletTransparentOutput}, zcash_primitives::{ - legacy::{keys::IncomingViewingKey, Script}, + legacy::{keys::IncomingViewingKey, Script, TransparentAddress}, transaction::components::{OutPoint, TxOut}, }, }; @@ -53,20 +50,13 @@ use { pub mod init; pub mod transact; -pub(crate) enum PoolType { - Transparent, - Sapling, -} - -impl PoolType { - pub(crate) fn typecode(&self) -> i64 { - // These constants are *incidentally* shared with the typecodes - // for unified addresses, but this is exclusively an internal - // implementation detail. - match self { - PoolType::Transparent => 0i64, - PoolType::Sapling => 2i64, - } +pub(crate) fn pool_code(pool_type: PoolType) -> i64 { + // These constants are *incidentally* shared with the typecodes + // for unified addresses, but this is exclusively an internal + // implementation detail. + match pool_type { + PoolType::Transparent => 0i64, + PoolType::Sapling => 2i64, } } @@ -1153,128 +1143,50 @@ pub fn update_expired_notes

( stmts.stmt_update_expired(height) } -/// Records information about a note that your wallet created. -#[deprecated( - note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletWrite::store_decrypted_tx instead." -)] -#[allow(deprecated)] -pub fn put_sent_note<'a, P: consensus::Parameters>( +/// Records information about a transaction output that your wallet created. +/// +/// This is a crate-internal convenience method. +pub(crate) fn insert_sent_output<'a, P: consensus::Parameters>( stmts: &mut DataConnStmtCache<'a, P>, tx_ref: i64, - output_index: usize, - account: AccountId, - to: &PaymentAddress, - value: Amount, - memo: Option<&MemoBytes>, + from_account: AccountId, + output: &SentTransactionOutput, ) -> Result<(), SqliteClientError> { - // Try updating an existing sent note. - if !stmts.stmt_update_sent_note( - account, - &encode_payment_address_p(&stmts.wallet_db.params, to), - value, - memo, + stmts.stmt_insert_sent_output( tx_ref, - PoolType::Sapling, - output_index, - )? { - // It isn't there, so insert. - insert_sent_note(stmts, tx_ref, output_index, account, to, value, memo)? - } - - Ok(()) -} - -/// Adds information about a sent transparent UTXO to the database if it does not already -/// exist, or updates it if a record for the UTXO already exists. -/// -/// `output_index` is the index within transparent UTXOs of the transaction that contains the recipient output. -#[deprecated( - note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletWrite::store_decrypted_tx instead." -)] -#[allow(deprecated)] -pub fn put_sent_utxo<'a, P: consensus::Parameters>( - stmts: &mut DataConnStmtCache<'a, P>, - tx_ref: i64, - output_index: usize, - account: AccountId, - to: &TransparentAddress, - value: Amount, -) -> Result<(), SqliteClientError> { - // Try updating an existing sent UTXO. - if !stmts.stmt_update_sent_note( - account, - &encode_transparent_address_p(&stmts.wallet_db.params, to), - value, - None, - tx_ref, - PoolType::Transparent, - output_index, - )? { - // It isn't there, so insert. - insert_sent_utxo(stmts, tx_ref, output_index, account, to, value)? - } - - Ok(()) -} - -/// Inserts a sent note into the wallet database. -/// -/// `output_index` is the index within the transaction that contains the recipient output: -/// -/// - If `to` is a Sapling address, this is an index into the Sapling outputs of the -/// transaction. -/// - If `to` is a transparent address, this is an index into the transparent outputs of -/// the transaction. -#[deprecated( - note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletWrite::store_sent_tx instead." -)] -pub fn insert_sent_note<'a, P: consensus::Parameters>( - stmts: &mut DataConnStmtCache<'a, P>, - tx_ref: i64, - output_index: usize, - account: AccountId, - to: &PaymentAddress, - value: Amount, - memo: Option<&MemoBytes>, -) -> Result<(), SqliteClientError> { - let to_str = encode_payment_address_p(&stmts.wallet_db.params, to); - - stmts.stmt_insert_sent_note( - tx_ref, - PoolType::Sapling, - output_index, - account, - &to_str, - value, - memo, + output.output_index, + from_account, + &output.recipient, + output.value, + output.memo.as_ref(), ) } -/// Inserts information about a sent transparent UTXO into the wallet database. +/// Records information about a transaction output that your wallet created. /// -/// `output_index` is the index within transparent UTXOs of the transaction that contains the recipient output. -#[deprecated( - note = "This method will be removed in a future update. Use zcash_client_backend::data_api::WalletWrite::store_sent_tx instead." -)] -pub fn insert_sent_utxo<'a, P: consensus::Parameters>( +/// This is a crate-internal convenience method. +#[allow(clippy::too_many_arguments)] +pub(crate) fn put_sent_output<'a, P: consensus::Parameters>( stmts: &mut DataConnStmtCache<'a, P>, + from_account: AccountId, tx_ref: i64, output_index: usize, - account: AccountId, - to: &TransparentAddress, + recipient: &Recipient, value: Amount, + memo: Option<&MemoBytes>, ) -> Result<(), SqliteClientError> { - let to_str = encode_transparent_address_p(&stmts.wallet_db.params, to); + if !stmts.stmt_update_sent_output(from_account, recipient, value, memo, tx_ref, output_index)? { + stmts.stmt_insert_sent_output( + tx_ref, + output_index, + from_account, + recipient, + value, + memo, + )?; + } - stmts.stmt_insert_sent_note( - tx_ref, - PoolType::Transparent, - output_index, - account, - &to_str, - value, - None, - ) + Ok(()) } #[cfg(test)] diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index cf0fb1fa3..cb50cb77c 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -122,8 +122,13 @@ fn init_wallet_db_internal( seed: Option>, target_migration: Option, ) -> Result<(), MigratorError> { + // Turn off foreign keys, and ensure that table replacement/modification + // does not break views wdb.conn - .execute("PRAGMA foreign_keys = OFF", []) + .execute_batch( + "PRAGMA foreign_keys = OFF; + PRAGMA legacy_alter_table = TRUE;", + ) .map_err(|e| MigratorError::Adapter(WalletMigrationError::from(e)))?; let adapter = RusqliteAdapter::new(&mut wdb.conn, Some("schemer_migrations".to_string())); adapter.init().expect("Migrations table setup succeeds."); @@ -311,7 +316,10 @@ mod tests { use super::{init_accounts_table, init_blocks_table, init_wallet_db}; #[cfg(feature = "transparent-inputs")] - use {crate::wallet::PoolType, zcash_primitives::legacy::keys as transparent}; + use { + crate::wallet::{pool_code, PoolType}, + zcash_primitives::legacy::keys as transparent, + }; #[test] fn verify_schema() { @@ -375,12 +383,17 @@ mod tests { output_pool INTEGER NOT NULL , output_index INTEGER NOT NULL, from_account INTEGER NOT NULL, - address TEXT NOT NULL, + to_address TEXT, + to_account INTEGER, value INTEGER NOT NULL, memo BLOB, FOREIGN KEY (tx) REFERENCES transactions(id_tx), FOREIGN KEY (from_account) REFERENCES accounts(account), - CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index) + FOREIGN KEY (to_account) REFERENCES accounts(account), + CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index), + CONSTRAINT note_recipient CHECK ( + (to_address IS NOT NULL) != (to_account IS NOT NULL) + ) )", "CREATE TABLE transactions ( id_tx INTEGER PRIMARY KEY, @@ -943,7 +956,7 @@ mod tests { wdb.conn.execute( "INSERT INTO sent_notes (tx, output_pool, output_index, from_account, address, value) VALUES (0, ?, 0, ?, ?, 0)", - [PoolType::Transparent.typecode().to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?; + [pool_code(PoolType::Transparent).to_sql()?, u32::from(account).to_sql()?, taddr.to_sql()?])?; } Ok(()) diff --git a/zcash_client_sqlite/src/wallet/init/migrations.rs b/zcash_client_sqlite/src/wallet/init/migrations.rs index 0f78213e4..a5b122f17 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations.rs @@ -2,6 +2,7 @@ mod add_transaction_views; mod add_utxo_account; mod addresses_table; mod initial_setup; +mod sent_notes_to_internal; mod ufvk_support; mod utxos_table; @@ -31,5 +32,6 @@ pub(super) fn all_migrations( Box::new(add_utxo_account::Migration { _params: params.clone(), }), + Box::new(sent_notes_to_internal::Migration {}), ] } diff --git a/zcash_client_sqlite/src/wallet/init/migrations/sent_notes_to_internal.rs b/zcash_client_sqlite/src/wallet/init/migrations/sent_notes_to_internal.rs new file mode 100644 index 000000000..c67777660 --- /dev/null +++ b/zcash_client_sqlite/src/wallet/init/migrations/sent_notes_to_internal.rs @@ -0,0 +1,89 @@ +//! A migration that adds an identifier for the account that received a sent note +//! on an internal address to the sent_notes table. +use std::collections::HashSet; + +use rusqlite; +use schemer; +use schemer_rusqlite::RusqliteMigration; +use uuid::Uuid; + +use super::{addresses_table, utxos_table}; +use crate::wallet::init::WalletMigrationError; + +/// This migration adds the `to_account` field to the `sent_notes` table. +/// +/// 0ddbe561-8259-4212-9ab7-66fdc4a74e1d +pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( + 0x0ddbe561, + 0x8259, + 0x4212, + b"\x9a\xb7\x66\xfd\xc4\xa7\x4e\x1d", +); + +pub(super) struct Migration; + +impl schemer::Migration for Migration { + fn id(&self) -> Uuid { + MIGRATION_ID + } + + fn dependencies(&self) -> HashSet { + [utxos_table::MIGRATION_ID, addresses_table::MIGRATION_ID] + .into_iter() + .collect() + } + + fn description(&self) -> &'static str { + "Adds an identifier for the account that received an internal note to the sent_notes table" + } +} + +impl RusqliteMigration for Migration { + type Error = WalletMigrationError; + + fn up(&self, transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // Adds the `to_account` column to the `sent_notes` table and establishes the + // foreign key relationship with the `account` table. + transaction.execute_batch( + "CREATE TABLE sent_notes_new ( + id_note INTEGER PRIMARY KEY, + tx INTEGER NOT NULL, + output_pool INTEGER NOT NULL , + output_index INTEGER NOT NULL, + from_account INTEGER NOT NULL, + to_address TEXT, + to_account INTEGER, + value INTEGER NOT NULL, + memo BLOB, + FOREIGN KEY (tx) REFERENCES transactions(id_tx), + FOREIGN KEY (from_account) REFERENCES accounts(account), + FOREIGN KEY (to_account) REFERENCES accounts(account), + CONSTRAINT tx_output UNIQUE (tx, output_pool, output_index), + CONSTRAINT note_recipient CHECK ( + (to_address IS NOT NULL) != (to_account IS NOT NULL) + ) + ); + INSERT INTO sent_notes_new ( + id_note, tx, output_pool, output_index, + from_account, to_address, + value, memo) + SELECT + id_note, tx, output_pool, output_index, + from_account, address, + value, memo + FROM sent_notes;", + )?; + + transaction.execute_batch( + "DROP TABLE sent_notes; + ALTER TABLE sent_notes_new RENAME TO sent_notes;", + )?; + + Ok(()) + } + + fn down(&self, _transaction: &rusqlite::Transaction) -> Result<(), WalletMigrationError> { + // TODO: something better than just panic? + panic!("Cannot revert this migration."); + } +} diff --git a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs index 24936b590..e4774bda8 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/ufvk_support.rs @@ -7,7 +7,9 @@ use schemer_rusqlite::RusqliteMigration; use secrecy::{ExposeSecret, SecretVec}; use uuid::Uuid; -use zcash_client_backend::{address::RecipientAddress, keys::UnifiedSpendingKey}; +use zcash_client_backend::{ + address::RecipientAddress, data_api::PoolType, keys::UnifiedSpendingKey, +}; use zcash_primitives::{consensus, zip32::AccountId}; #[cfg(feature = "transparent-inputs")] @@ -18,7 +20,7 @@ use zcash_client_backend::encoding::AddressCodec; use crate::wallet::{ init::{migrations::utxos_table, WalletMigrationError}, - PoolType, + pool_code, }; pub(super) const MIGRATION_ID: Uuid = Uuid::from_fields( @@ -227,8 +229,8 @@ impl RusqliteMigration for Migration

{ )) })?; let output_pool = match decoded_address { - RecipientAddress::Shielded(_) => Ok(PoolType::Sapling.typecode()), - RecipientAddress::Transparent(_) => Ok(PoolType::Transparent.typecode()), + RecipientAddress::Shielded(_) => Ok(pool_code(PoolType::Sapling)), + RecipientAddress::Transparent(_) => Ok(pool_code(PoolType::Transparent)), RecipientAddress::Unified(_) => Err(WalletMigrationError::CorruptedData( "Unified addresses should not yet appear in the sent_notes table." .to_string(),