Merge pull request #659 from nuttycom/wallet/decrypt_with_internal_key

Track inputs sent to wallet-internal recipients
This commit is contained in:
Kris Nuttycombe 2022-10-11 16:57:54 -06:00 committed by GitHub
commit f689018fcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 510 additions and 373 deletions

View File

@ -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<Payment>`.
- `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:

View File

@ -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<SentTransactionOutput<'a>>,
pub outputs: Vec<SentTransactionOutput>,
pub fee_amount: Amount,
#[cfg(feature = "transparent-inputs")]
pub utxos_spent: Vec<OutPoint>,
}
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<MemoBytes>,
}

View File

@ -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,

View File

@ -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<P: consensus::Parameters>(
tx: &Transaction,
ufvks: &HashMap<AccountId, UnifiedFullViewingKey>,
) -> Vec<DecryptedOutput> {
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()
}

View File

@ -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<P: consensus::Parameters> AddressCodec<P> for TransparentAddress {
}
}
impl<P: consensus::Parameters> AddressCodec<P> 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<Self, Bech32DecodeError> {
decode_payment_address(params.hrp_sapling_payment_address(), address)
}
}
impl<P: consensus::Parameters> AddressCodec<P> for UnifiedAddress {
type Error = String;
fn encode(&self, params: &P) -> String {
self.encode(params)
}
fn decode(params: &P, address: &str) -> Result<Self, String> {
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<P: consensus::Parameters>(
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<P: consensus::Parameters>(
/// 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<P: consensus::Parameters>(
///
/// 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<PaymentAddress, Bech32DecodeError> {
pub fn decode_payment_address(
hrp: &str,
s: &str,
) -> Result<sapling::PaymentAddress, Bech32DecodeError> {
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<PaymentAddress, Bech
let mut bytes = [0; 43];
bytes.copy_from_slice(&data);
PaymentAddress::from_bytes(&bytes)
sapling::PaymentAddress::from_bytes(&bytes)
})
}
@ -304,14 +346,14 @@ pub fn decode_payment_address(hrp: &str, s: &str) -> Result<PaymentAddress, Bech
/// encoding::encode_transparent_address,
/// };
/// use zcash_primitives::{
/// constants::testnet::{B58_PUBKEY_ADDRESS_PREFIX, B58_SCRIPT_ADDRESS_PREFIX},
/// consensus::{TEST_NETWORK, Parameters},
/// legacy::TransparentAddress,
/// };
///
/// assert_eq!(
/// encode_transparent_address(
/// &B58_PUBKEY_ADDRESS_PREFIX,
/// &B58_SCRIPT_ADDRESS_PREFIX,
/// &TEST_NETWORK.b58_pubkey_address_prefix(),
/// &TEST_NETWORK.b58_script_address_prefix(),
/// &TransparentAddress::PublicKey([0; 20]),
/// ),
/// "tm9iMLAuYMzJ6jtFLcA7rzUmfreGuKvr7Ma",
@ -319,8 +361,8 @@ pub fn decode_payment_address(hrp: &str, s: &str) -> Result<PaymentAddress, Bech
///
/// assert_eq!(
/// encode_transparent_address(
/// &B58_PUBKEY_ADDRESS_PREFIX,
/// &B58_SCRIPT_ADDRESS_PREFIX,
/// &TEST_NETWORK.b58_pubkey_address_prefix(),
/// &TEST_NETWORK.b58_script_address_prefix(),
/// &TransparentAddress::Script([0; 20]),
/// ),
/// "t26YoyZ1iPgiMEWL4zGUm74eVWfhyDMXzY2",
@ -369,7 +411,7 @@ pub fn encode_transparent_address_p<P: consensus::Parameters>(
///
/// ```
/// 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<P: consensus::Parameters>(
///
/// 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<P: consensus::Parameters>(
///
/// 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]))),

View File

@ -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};

View File

@ -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`

View File

@ -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<AccountId> = 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.

View File

@ -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<bool, 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),
};
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<bool, SqliteClientError> {
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.
///

View File

@ -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<P>(
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)]

View File

@ -122,8 +122,13 @@ fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
seed: Option<SecretVec<u8>>,
target_migration: Option<Uuid>,
) -> Result<(), MigratorError<WalletMigrationError>> {
// 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(())

View File

@ -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<P: consensus::Parameters + 'static>(
Box::new(add_utxo_account::Migration {
_params: params.clone(),
}),
Box::new(sent_notes_to_internal::Migration {}),
]
}

View File

@ -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<Uuid> {
[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.");
}
}

View File

@ -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<P: consensus::Parameters> RusqliteMigration for Migration<P> {
))
})?;
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(),