Merge pull request #623 from zcash/489-addresses-table

`zcash_client_*`: Various changes to how addresses are handled
This commit is contained in:
Kris Nuttycombe 2022-09-08 18:51:16 -06:00 committed by GitHub
commit 1839696c75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 451 additions and 53 deletions

View File

@ -32,6 +32,7 @@ and this library adheres to Rust's notion of
- `RecipientAddress::Unified` - `RecipientAddress::Unified`
- `zcash_client_backend::data_api`: - `zcash_client_backend::data_api`:
- `WalletRead::get_unified_full_viewing_keys` - `WalletRead::get_unified_full_viewing_keys`
- `WalletRead::get_all_nullifiers`
- `WalletWrite::remove_unmined_tx` (behind the `unstable` feature flag). - `WalletWrite::remove_unmined_tx` (behind the `unstable` feature flag).
- `zcash_client_backend::proto`: - `zcash_client_backend::proto`:
- `actions` field on `compact_formats::CompactTx` - `actions` field on `compact_formats::CompactTx`
@ -78,13 +79,12 @@ and this library adheres to Rust's notion of
a `min_confirmations` argument that is used to compute an upper bound on a `min_confirmations` argument that is used to compute an upper bound on
the anchor height being returned; this had previously been hardcoded to the anchor height being returned; this had previously been hardcoded to
`data_api::wallet::ANCHOR_OFFSET`. `data_api::wallet::ANCHOR_OFFSET`.
- `WalletRead::get_address` now returns a `UnifiedAddress` instead of a
`sapling::PaymentAddress`.
- `WalletRead::get_spendable_notes` has been renamed to - `WalletRead::get_spendable_notes` has been renamed to
`get_spendable_sapling_notes` `get_spendable_sapling_notes`
- `WalletRead::select_spendable_notes` has been renamed to - `WalletRead::select_spendable_notes` has been renamed to
`select_spendable_sapling_notes` `select_spendable_sapling_notes`
- `WalletRead::get_all_nullifiers` has been
added. This method provides access to all Sapling nullifiers, including
for notes that have been previously marked spent.
- The `zcash_client_backend::data_api::SentTransaction` type has been - The `zcash_client_backend::data_api::SentTransaction` type has been
substantially modified to accommodate handling of transparent inputs. substantially modified to accommodate handling of transparent inputs.
Per-output data has been split out into a new struct `SentTransactionOutput` Per-output data has been split out into a new struct `SentTransactionOutput`

View File

@ -4,18 +4,21 @@ use std::cmp;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Debug; use std::fmt::Debug;
#[cfg(feature = "transparent-inputs")]
use std::collections::HashSet;
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
consensus::BlockHeight, consensus::BlockHeight,
memo::{Memo, MemoBytes}, memo::{Memo, MemoBytes},
merkle_tree::{CommitmentTree, IncrementalWitness}, merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{Node, Nullifier, PaymentAddress}, sapling::{Node, Nullifier},
transaction::{components::Amount, Transaction, TxId}, transaction::{components::Amount, Transaction, TxId},
zip32::{AccountId, ExtendedFullViewingKey}, zip32::{AccountId, ExtendedFullViewingKey},
}; };
use crate::{ use crate::{
address::RecipientAddress, address::{RecipientAddress, UnifiedAddress},
decrypt::DecryptedOutput, decrypt::DecryptedOutput,
keys::UnifiedFullViewingKey, keys::UnifiedFullViewingKey,
proto::compact_formats::CompactBlock, proto::compact_formats::CompactBlock,
@ -111,13 +114,12 @@ pub trait WalletRead {
/// or `Ok(None)` if the transaction is not mined in the main chain. /// or `Ok(None)` if the transaction is not mined in the main chain.
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error>; fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error>;
/// Returns the payment address for the specified account, if the account /// Returns the unified address for the specified account, if the account
/// identifier specified refers to a valid account for this wallet. /// identifier specified refers to a valid account for this wallet.
/// ///
/// This will return `Ok(None)` if the account identifier does not correspond /// This will return `Ok(None)` if the account identifier does not correspond
/// to a known account. /// to a known account.
// TODO: This does not appear to be the case. fn get_address(&self, account: AccountId) -> Result<Option<UnifiedAddress>, Self::Error>;
fn get_address(&self, account: AccountId) -> Result<Option<PaymentAddress>, Self::Error>;
/// Returns all unified full viewing keys known to this wallet. /// Returns all unified full viewing keys known to this wallet.
fn get_unified_full_viewing_keys( fn get_unified_full_viewing_keys(
@ -194,6 +196,16 @@ pub trait WalletRead {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
pub trait WalletReadTransparent: WalletRead { pub trait WalletReadTransparent: WalletRead {
/// Returns the set of all transparent receivers associated with the given account.
///
/// The set contains all transparent receivers that are known to have been derived
/// under this account. Wallets should scan the chain for UTXOs sent to these
/// receivers.
fn get_transparent_receivers(
&self,
account: AccountId,
) -> Result<HashSet<TransparentAddress>, Self::Error>;
/// Returns a list of unspent transparent UTXOs that appear in the chain at heights up to and /// Returns a list of unspent transparent UTXOs that appear in the chain at heights up to and
/// including `max_height`. /// including `max_height`.
fn get_unspent_transparent_outputs( fn get_unspent_transparent_outputs(
@ -327,18 +339,22 @@ pub trait BlockSource {
pub mod testing { pub mod testing {
use std::collections::HashMap; use std::collections::HashMap;
#[cfg(feature = "transparent-inputs")]
use std::collections::HashSet;
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
consensus::BlockHeight, consensus::BlockHeight,
legacy::TransparentAddress, legacy::TransparentAddress,
memo::Memo, memo::Memo,
merkle_tree::{CommitmentTree, IncrementalWitness}, merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{Node, Nullifier, PaymentAddress}, sapling::{Node, Nullifier},
transaction::{components::Amount, Transaction, TxId}, transaction::{components::Amount, Transaction, TxId},
zip32::{AccountId, ExtendedFullViewingKey}, zip32::{AccountId, ExtendedFullViewingKey},
}; };
use crate::{ use crate::{
address::UnifiedAddress,
keys::UnifiedFullViewingKey, keys::UnifiedFullViewingKey,
proto::compact_formats::CompactBlock, proto::compact_formats::CompactBlock,
wallet::{SpendableNote, WalletTransparentOutput}, wallet::{SpendableNote, WalletTransparentOutput},
@ -392,7 +408,7 @@ pub mod testing {
Ok(None) Ok(None)
} }
fn get_address(&self, _account: AccountId) -> Result<Option<PaymentAddress>, Self::Error> { fn get_address(&self, _account: AccountId) -> Result<Option<UnifiedAddress>, Self::Error> {
Ok(None) Ok(None)
} }
@ -469,6 +485,13 @@ pub mod testing {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
impl WalletReadTransparent for MockWalletDb { impl WalletReadTransparent for MockWalletDb {
fn get_transparent_receivers(
&self,
_account: AccountId,
) -> Result<HashSet<TransparentAddress>, Self::Error> {
Ok(HashSet::new())
}
fn get_unspent_transparent_outputs( fn get_unspent_transparent_outputs(
&self, &self,
_address: &TransparentAddress, _address: &TransparentAddress,

View File

@ -29,6 +29,12 @@ and this library adheres to Rust's notion of
column. Values for this column should be derived from the wallet's seed and column. Values for this column should be derived from the wallet's seed and
the account number; the Sapling component of the resulting Unified Full the account number; the Sapling component of the resulting Unified Full
Viewing Key should match the old value in the `extfvk` column. Viewing Key should match the old value in the `extfvk` column.
- The `address` and `transparent_address` columns of the `accounts` table have
been removed.
- A new `addresses` table stores Unified Addresses, keyed on their `account`
and `diversifier_index`, to enable storing diversifed Unified Addresses.
- Transparent addresses for an account should be obtained by extracting the
transparent receiver of a Unified Address for the account.
- A new non-null column, `output_pool` has been added to the `sent_notes` - A new non-null column, `output_pool` has been added to the `sent_notes`
table to enable distinguishing between Sapling and transparent outputs table to enable distinguishing between Sapling and transparent outputs
(and in the future, outputs to other pools). Values for this column should (and in the future, outputs to other pools). Values for this column should
@ -66,6 +72,11 @@ and this library adheres to Rust's notion of
`zcash_client_backend::data_api::WalletRead::get_unified_full_viewing_keys` `zcash_client_backend::data_api::WalletRead::get_unified_full_viewing_keys`
instead). instead).
### Fixed
- The `zcash_client_backend::data_api::WalletRead::get_address` implementation
for `zcash_client_sqlite::WalletDb` now correctly returns `Ok(None)` if the
account identifier does not correspond to a known account.
### Deprecated ### Deprecated
- A number of public API methods that are used internally to support the - A number of public API methods that are used internally to support the
`zcash_client_backend::data_api::{WalletRead, WalletWrite}` interfaces have `zcash_client_backend::data_api::{WalletRead, WalletWrite}` interfaces have

View File

@ -18,6 +18,7 @@ bech32 = "0.8"
bs58 = { version = "0.4", features = ["check"] } bs58 = { version = "0.4", features = ["check"] }
ff = "0.12" ff = "0.12"
group = "0.12" group = "0.12"
hdwallet = { version = "0.3.1", optional = true }
jubjub = "0.9" jubjub = "0.9"
protobuf = "~2.27.1" # MSRV 1.52.1 protobuf = "~2.27.1" # MSRV 1.52.1
rand_core = "0.6" rand_core = "0.6"
@ -38,7 +39,7 @@ zcash_proofs = { version = "0.7", path = "../zcash_proofs" }
[features] [features]
mainnet = [] mainnet = []
test-dependencies = ["zcash_client_backend/test-dependencies"] test-dependencies = ["zcash_client_backend/test-dependencies"]
transparent-inputs = ["zcash_client_backend/transparent-inputs"] transparent-inputs = ["hdwallet", "zcash_client_backend/transparent-inputs"]
unstable = ["zcash_client_backend/unstable"] unstable = ["zcash_client_backend/unstable"]
[lib] [lib]

View File

@ -43,6 +43,9 @@ pub enum SqliteClientError {
/// Base58 decoding error /// Base58 decoding error
Base58(bs58::decode::Error), Base58(bs58::decode::Error),
#[cfg(feature = "transparent-inputs")]
HdwalletError(hdwallet::error::Error),
/// Base58 decoding error /// Base58 decoding error
TransparentAddress(TransparentCodecError), TransparentAddress(TransparentCodecError),
@ -90,6 +93,8 @@ impl fmt::Display for SqliteClientError {
write!(f, "A rewind must be either of less than {} blocks, or at least back to block {} for your wallet; the requested height was {}.", PRUNING_HEIGHT, h, r), write!(f, "A rewind must be either of less than {} blocks, or at least back to block {} for your wallet; the requested height was {}.", PRUNING_HEIGHT, h, r),
SqliteClientError::Bech32DecodeError(e) => write!(f, "{}", e), SqliteClientError::Bech32DecodeError(e) => write!(f, "{}", e),
SqliteClientError::Base58(e) => write!(f, "{}", e), SqliteClientError::Base58(e) => write!(f, "{}", e),
#[cfg(feature = "transparent-inputs")]
SqliteClientError::HdwalletError(e) => write!(f, "{:?}", e),
SqliteClientError::TransparentAddress(e) => write!(f, "{}", e), SqliteClientError::TransparentAddress(e) => write!(f, "{}", e),
SqliteClientError::TableNotEmpty => write!(f, "Table is not empty"), SqliteClientError::TableNotEmpty => write!(f, "Table is not empty"),
#[cfg(feature = "unstable")] #[cfg(feature = "unstable")]
@ -126,6 +131,13 @@ impl From<bs58::decode::Error> for SqliteClientError {
} }
} }
#[cfg(feature = "transparent-inputs")]
impl From<hdwallet::error::Error> for SqliteClientError {
fn from(e: hdwallet::error::Error) -> Self {
SqliteClientError::HdwalletError(e)
}
}
impl From<zcash_primitives::memo::Error> for SqliteClientError { impl From<zcash_primitives::memo::Error> for SqliteClientError {
fn from(e: zcash_primitives::memo::Error) -> Self { fn from(e: zcash_primitives::memo::Error) -> Self {
SqliteClientError::InvalidMemo(e) SqliteClientError::InvalidMemo(e)

View File

@ -36,6 +36,9 @@ use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::path::Path; use std::path::Path;
#[cfg(feature = "transparent-inputs")]
use std::collections::HashSet;
use rusqlite::{Connection, NO_PARAMS}; use rusqlite::{Connection, NO_PARAMS};
use zcash_primitives::{ use zcash_primitives::{
@ -43,13 +46,13 @@ use zcash_primitives::{
consensus::{self, BlockHeight}, consensus::{self, BlockHeight},
memo::Memo, memo::Memo,
merkle_tree::{CommitmentTree, IncrementalWitness}, merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{Node, Nullifier, PaymentAddress}, sapling::{Node, Nullifier},
transaction::{components::Amount, Transaction, TxId}, transaction::{components::Amount, Transaction, TxId},
zip32::{AccountId, ExtendedFullViewingKey}, zip32::{AccountId, ExtendedFullViewingKey},
}; };
use zcash_client_backend::{ use zcash_client_backend::{
address::RecipientAddress, address::{RecipientAddress, UnifiedAddress},
data_api::{ data_api::{
BlockSource, DecryptedTransaction, PrunedBlock, SentTransaction, WalletRead, WalletWrite, BlockSource, DecryptedTransaction, PrunedBlock, SentTransaction, WalletRead, WalletWrite,
}, },
@ -150,9 +153,8 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
wallet::get_unified_full_viewing_keys(self) wallet::get_unified_full_viewing_keys(self)
} }
fn get_address(&self, account: AccountId) -> Result<Option<PaymentAddress>, Self::Error> { fn get_address(&self, account: AccountId) -> Result<Option<UnifiedAddress>, Self::Error> {
#[allow(deprecated)] wallet::get_address_ua(self, account)
wallet::get_address(self, account)
} }
fn is_valid_account_extfvk( fn is_valid_account_extfvk(
@ -235,6 +237,13 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
impl<P: consensus::Parameters> WalletReadTransparent for WalletDb<P> { impl<P: consensus::Parameters> WalletReadTransparent for WalletDb<P> {
fn get_transparent_receivers(
&self,
account: AccountId,
) -> Result<HashSet<TransparentAddress>, Self::Error> {
wallet::get_transparent_receivers(self, account)
}
fn get_unspent_transparent_outputs( fn get_unspent_transparent_outputs(
&self, &self,
address: &TransparentAddress, address: &TransparentAddress,
@ -267,7 +276,7 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
self.wallet_db.get_unified_full_viewing_keys() self.wallet_db.get_unified_full_viewing_keys()
} }
fn get_address(&self, account: AccountId) -> Result<Option<PaymentAddress>, Self::Error> { fn get_address(&self, account: AccountId) -> Result<Option<UnifiedAddress>, Self::Error> {
self.wallet_db.get_address(account) self.wallet_db.get_address(account)
} }
@ -340,6 +349,13 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
impl<'a, P: consensus::Parameters> WalletReadTransparent for DataConnStmtCache<'a, P> { impl<'a, P: consensus::Parameters> WalletReadTransparent for DataConnStmtCache<'a, P> {
fn get_transparent_receivers(
&self,
account: AccountId,
) -> Result<HashSet<TransparentAddress>, Self::Error> {
self.wallet_db.get_transparent_receivers(account)
}
fn get_unspent_transparent_outputs( fn get_unspent_transparent_outputs(
&self, &self,
address: &TransparentAddress, address: &TransparentAddress,
@ -725,6 +741,14 @@ mod tests {
pub(crate) fn init_test_accounts_table( pub(crate) fn init_test_accounts_table(
db_data: &WalletDb<Network>, db_data: &WalletDb<Network>,
) -> (DiversifiableFullViewingKey, Option<TransparentAddress>) { ) -> (DiversifiableFullViewingKey, Option<TransparentAddress>) {
let (ufvk, taddr) = init_test_accounts_table_ufvk(db_data);
(ufvk.sapling().unwrap().clone(), taddr)
}
#[cfg(test)]
pub(crate) fn init_test_accounts_table_ufvk(
db_data: &WalletDb<Network>,
) -> (UnifiedFullViewingKey, Option<TransparentAddress>) {
let seed = [0u8; 32]; let seed = [0u8; 32];
let account = AccountId::from(0); let account = AccountId::from(0);
let extsk = sapling::spending_key(&seed, network().coin_type(), account); let extsk = sapling::spending_key(&seed, network().coin_type(), account);
@ -745,15 +769,15 @@ mod tests {
let ufvk = UnifiedFullViewingKey::new( let ufvk = UnifiedFullViewingKey::new(
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
tkey, tkey,
Some(dfvk.clone()), Some(dfvk),
None, None,
) )
.unwrap(); .unwrap();
let ufvks = HashMap::from([(account, ufvk)]); let ufvks = HashMap::from([(account, ufvk.clone())]);
init_accounts_table(db_data, &ufvks).unwrap(); init_accounts_table(db_data, &ufvks).unwrap();
(dfvk, taddr) (ufvk, taddr)
} }
/// Create a fake CompactBlock at the given height, containing a single output paying /// Create a fake CompactBlock at the given height, containing a single output paying
@ -901,6 +925,36 @@ mod tests {
.unwrap(); .unwrap();
} }
#[cfg(feature = "transparent-inputs")]
#[test]
fn transparent_receivers() {
use secrecy::Secret;
use tempfile::NamedTempFile;
use zcash_client_backend::data_api::WalletReadTransparent;
use crate::{chain::init::init_cache_database, wallet::init::init_wallet_db};
let cache_file = NamedTempFile::new().unwrap();
let db_cache = BlockDb::for_path(cache_file.path()).unwrap();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet.
let (ufvk, taddr) = init_test_accounts_table_ufvk(&db_data);
let taddr = taddr.unwrap();
let receivers = db_data.get_transparent_receivers(0.into()).unwrap();
// The receiver for the default UA should be in the set.
assert!(receivers.contains(ufvk.default_address().0.transparent().unwrap()));
// The default t-addr should be in the set.
assert!(receivers.contains(&taddr));
}
#[cfg(feature = "unstable")] #[cfg(feature = "unstable")]
#[test] #[test]
fn remove_unmined_tx_reverts_balance() { fn remove_unmined_tx_reverts_balance() {

View File

@ -12,6 +12,9 @@ use rusqlite::{OptionalExtension, ToSql, NO_PARAMS};
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryFrom; use std::convert::TryFrom;
#[cfg(feature = "transparent-inputs")]
use std::collections::HashSet;
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters}, consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters},
@ -23,7 +26,7 @@ use zcash_primitives::{
}; };
use zcash_client_backend::{ use zcash_client_backend::{
address::RecipientAddress, address::{RecipientAddress, UnifiedAddress},
data_api::error::Error, data_api::error::Error,
encoding::{encode_payment_address_p, encode_transparent_address_p}, encoding::{encode_payment_address_p, encode_transparent_address_p},
keys::UnifiedFullViewingKey, keys::UnifiedFullViewingKey,
@ -41,7 +44,7 @@ use {
rusqlite::params, rusqlite::params,
zcash_client_backend::{encoding::AddressCodec, wallet::WalletTransparentOutput}, zcash_client_backend::{encoding::AddressCodec, wallet::WalletTransparentOutput},
zcash_primitives::{ zcash_primitives::{
legacy::Script, legacy::{keys::IncomingViewingKey, Script},
transaction::components::{OutPoint, TxOut}, transaction::components::{OutPoint, TxOut},
}, },
}; };
@ -155,8 +158,9 @@ pub fn get_address<P: consensus::Parameters>(
wdb: &WalletDb<P>, wdb: &WalletDb<P>,
account: AccountId, account: AccountId,
) -> Result<Option<PaymentAddress>, SqliteClientError> { ) -> Result<Option<PaymentAddress>, SqliteClientError> {
// This returns the first diversified address, which will be the default one.
let addr: String = wdb.conn.query_row( let addr: String = wdb.conn.query_row(
"SELECT address FROM accounts "SELECT address FROM addresses
WHERE account = ?", WHERE account = ?",
&[u32::from(account)], &[u32::from(account)],
|row| row.get(0), |row| row.get(0),
@ -173,6 +177,86 @@ pub fn get_address<P: consensus::Parameters>(
}) })
} }
pub(crate) fn get_address_ua<P: consensus::Parameters>(
wdb: &WalletDb<P>,
account: AccountId,
) -> Result<Option<UnifiedAddress>, SqliteClientError> {
// This returns the first diversified address, which will be the default one.
let addr: Option<String> = wdb
.conn
.query_row_named(
"SELECT address FROM addresses WHERE account = :account",
&[(":account", &u32::from(account))],
|row| row.get(0),
)
.optional()?;
addr.map(|addr_str| {
RecipientAddress::decode(&wdb.params, &addr_str)
.ok_or_else(|| {
SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
})
.and_then(|addr| match addr {
RecipientAddress::Unified(ua) => Ok(ua),
_ => Err(SqliteClientError::CorruptedData(format!(
"Addresses table contains {} which is not a unified address",
addr_str,
))),
})
})
.transpose()
}
#[cfg(feature = "transparent-inputs")]
pub(crate) fn get_transparent_receivers<P: consensus::Parameters>(
wdb: &WalletDb<P>,
account: AccountId,
) -> Result<HashSet<TransparentAddress>, SqliteClientError> {
let mut ret = HashSet::new();
// Get all UAs derived
let mut ua_query = wdb
.conn
.prepare("SELECT address FROM addresses WHERE account = :account")?;
let mut rows = ua_query.query_named(&[(":account", &u32::from(account))])?;
while let Some(row) = rows.next()? {
let ua_str: String = row.get(0)?;
let ua = RecipientAddress::decode(&wdb.params, &ua_str)
.ok_or_else(|| {
SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
})
.and_then(|addr| match addr {
RecipientAddress::Unified(ua) => Ok(ua),
_ => Err(SqliteClientError::CorruptedData(format!(
"Addresses table contains {} which is not a unified address",
ua_str,
))),
})?;
if let Some(taddr) = ua.transparent() {
ret.insert(*taddr);
}
}
// Get the UFVK for the account.
let ufvk_str: String = wdb.conn.query_row(
"SELECT ufvk FROM accounts WHERE account = :account",
&[u32::from(account)],
|row| row.get(0),
)?;
let ufvk = UnifiedFullViewingKey::decode(&wdb.params, &ufvk_str)
.map_err(SqliteClientError::CorruptedData)?;
// Derive the default transparent address (if it wasn't already part of a derived UA).
if let Some(tfvk) = ufvk.transparent() {
let tivk = tfvk.derive_external_ivk()?;
let taddr = tivk.default_address().0;
ret.insert(taddr);
}
Ok(ret)
}
/// Returns the [`UnifiedFullViewingKey`]s for the wallet. /// Returns the [`UnifiedFullViewingKey`]s for the wallet.
pub(crate) fn get_unified_full_viewing_keys<P: consensus::Parameters>( pub(crate) fn get_unified_full_viewing_keys<P: consensus::Parameters>(
wdb: &WalletDb<P>, wdb: &WalletDb<P>,

View File

@ -26,6 +26,8 @@ use {
zcash_primitives::legacy::keys::IncomingViewingKey, zcash_primitives::legacy::keys::IncomingViewingKey,
}; };
mod migrations;
#[derive(Debug)] #[derive(Debug)]
pub enum WalletMigrationError { pub enum WalletMigrationError {
/// The seed is required for the migration. /// The seed is required for the migration.
@ -289,12 +291,34 @@ impl<P: consensus::Parameters> RusqliteMigration for WalletMigration2<P> {
} }
} }
add_account_internal::<P, WalletMigrationError>( let ufvk_str: String = ufvk.encode(&self.params);
&self.params, let address_str: String = ufvk.default_address().0.encode(&self.params);
transaction,
"accounts_new", // This migration, and the wallet behaviour before it, stored the default
account, // transparent address in the `accounts` table. This does not necessarily
&ufvk, // match the transparent receiver in the default Unified Address. Starting
// from `AddressesTableMigration` below, we no longer store transparent
// addresses directly, but instead extract them from the Unified Address
// (or from the UFVK if the UA was derived without a transparent receiver,
// which is not the case for UAs generated by this crate).
#[cfg(feature = "transparent-inputs")]
let taddress_str: Option<String> = ufvk.transparent().and_then(|k| {
k.derive_external_ivk()
.ok()
.map(|k| k.default_address().0.encode(&self.params))
});
#[cfg(not(feature = "transparent-inputs"))]
let taddress_str: Option<String> = None;
transaction.execute_named(
"INSERT INTO accounts_new (account, ufvk, address, transparent_address)
VALUES (:account, :ufvk, :address, :transparent_address)",
&[
(":account", &<u32>::from(account)),
(":ufvk", &ufvk_str),
(":address", &address_str),
(":transparent_address", &taddress_str),
],
)?; )?;
} else { } else {
return Err(WalletMigrationError::SeedRequired); return Err(WalletMigrationError::SeedRequired);
@ -463,9 +487,12 @@ pub fn init_wallet_db<P: consensus::Parameters + 'static>(
params: wdb.params.clone(), params: wdb.params.clone(),
seed, seed,
}); });
let addrs_migration = Box::new(migrations::AddressesTableMigration {
params: wdb.params.clone(),
});
migrator migrator
.register_multiple(vec![migration0, migration1, migration2]) .register_multiple(vec![migration0, migration1, migration2, addrs_migration])
.expect("Wallet migration registration should have been successful."); .expect("Wallet migration registration should have been successful.");
migrator.up(None)?; migrator.up(None)?;
wdb.conn wdb.conn
@ -557,23 +584,25 @@ fn add_account_internal<P: consensus::Parameters, E: From<rusqlite::Error>>(
key: &UnifiedFullViewingKey, key: &UnifiedFullViewingKey,
) -> Result<(), E> { ) -> Result<(), E> {
let ufvk_str: String = key.encode(network); let ufvk_str: String = key.encode(network);
let address_str: String = key.default_address().0.encode(network); conn.execute_named(
#[cfg(feature = "transparent-inputs")]
let taddress_str: Option<String> = key.transparent().and_then(|k| {
k.derive_external_ivk()
.ok()
.map(|k| k.default_address().0.encode(network))
});
#[cfg(not(feature = "transparent-inputs"))]
let taddress_str: Option<String> = None;
conn.execute(
&format!( &format!(
"INSERT INTO {} (account, ufvk, address, transparent_address) "INSERT INTO {} (account, ufvk) VALUES (:account, :ufvk)",
VALUES (?, ?, ?, ?)",
accounts_table accounts_table
), ),
params![<u32>::from(account), ufvk_str, address_str, taddress_str], &[(":account", &<u32>::from(account)), (":ufvk", &ufvk_str)],
)?;
// Always derive the default Unified Address for the account.
let (address, idx) = key.default_address();
let address_str: String = address.encode(network);
conn.execute_named(
"INSERT INTO addresses (account, diversifier_index, address)
VALUES (:account, :diversifier_index, :address)",
&[
(":account", &<u32>::from(account)),
(":diversifier_index", &&idx.0[..]),
(":address", &address_str),
],
)?; )?;
Ok(()) Ok(())
@ -683,9 +712,14 @@ mod tests {
let expected = vec![ let expected = vec![
"CREATE TABLE \"accounts\" ( "CREATE TABLE \"accounts\" (
account INTEGER PRIMARY KEY, account INTEGER PRIMARY KEY,
ufvk TEXT NOT NULL, ufvk TEXT NOT NULL
address TEXT, )",
transparent_address TEXT "CREATE TABLE addresses (
account INTEGER NOT NULL,
diversifier_index BLOB NOT NULL,
address TEXT NOT NULL,
FOREIGN KEY (account) REFERENCES accounts(account),
CONSTRAINT diversification UNIQUE (account, diversifier_index)
)", )",
"CREATE TABLE blocks ( "CREATE TABLE blocks (
height INTEGER PRIMARY KEY, height INTEGER PRIMARY KEY,
@ -1148,10 +1182,9 @@ mod tests {
// add a transparent "sent note" // add a transparent "sent note"
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
{ {
let taddr = RecipientAddress::Transparent( let taddr =
ufvk.default_address().0.transparent().unwrap().clone(), RecipientAddress::Transparent(*ufvk.default_address().0.transparent().unwrap())
) .encode(&tests::network());
.encode(&tests::network());
wdb.conn.execute( wdb.conn.execute(
"INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, '')", "INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, '')",
NO_PARAMS, NO_PARAMS,

View File

@ -0,0 +1,2 @@
mod addresses_table;
pub(super) use addresses_table::AddressesTableMigration;

View File

@ -0,0 +1,176 @@
use std::collections::HashSet;
use rusqlite::{Transaction, NO_PARAMS};
use schemer::Migration;
use schemer_rusqlite::RusqliteMigration;
use uuid::Uuid;
use zcash_client_backend::{address::RecipientAddress, keys::UnifiedFullViewingKey};
use zcash_primitives::{consensus, zip32::AccountId};
use super::super::{add_account_internal, WalletMigrationError};
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::legacy::keys::IncomingViewingKey;
/// The migration that removed the address columns from the `accounts` table, and created
/// the `accounts` table.
///
/// d956978c-9c87-4d6e-815d-fb8f088d094c
pub(super) const ADDRESSES_TABLE_MIGRATION: Uuid = Uuid::from_fields(
0xd956978c,
0x9c87,
0x4d6e,
b"\x81\x5d\xfb\x8f\x08\x8d\x09\x4c",
);
pub(crate) struct AddressesTableMigration<P: consensus::Parameters> {
pub(crate) params: P,
}
impl<P: consensus::Parameters> Migration for AddressesTableMigration<P> {
fn id(&self) -> Uuid {
ADDRESSES_TABLE_MIGRATION
}
fn dependencies(&self) -> HashSet<Uuid> {
["be57ef3b-388e-42ea-97e2-678dafcf9754"]
.iter()
.map(|uuidstr| ::uuid::Uuid::parse_str(uuidstr).unwrap())
.collect()
}
fn description(&self) -> &'static str {
"Adds the addresses table for tracking diversified UAs"
}
}
impl<P: consensus::Parameters> RusqliteMigration for AddressesTableMigration<P> {
type Error = WalletMigrationError;
fn up(&self, transaction: &Transaction) -> Result<(), WalletMigrationError> {
transaction.execute_batch(
"CREATE TABLE addresses (
account INTEGER NOT NULL,
diversifier_index BLOB NOT NULL,
address TEXT NOT NULL,
FOREIGN KEY (account) REFERENCES accounts(account),
CONSTRAINT diversification UNIQUE (account, diversifier_index)
);
CREATE TABLE accounts_new (
account INTEGER PRIMARY KEY,
ufvk TEXT NOT NULL
);",
)?;
let mut stmt_fetch_accounts = transaction
.prepare("SELECT account, ufvk, address, transparent_address FROM accounts")?;
let mut rows = stmt_fetch_accounts.query(NO_PARAMS)?;
while let Some(row) = rows.next()? {
let account: u32 = row.get(0)?;
let account = AccountId::from(account);
let ufvk_str: String = row.get(1)?;
let ufvk = UnifiedFullViewingKey::decode(&self.params, &ufvk_str)
.map_err(WalletMigrationError::CorruptedData)?;
// Verify that the address column contains the expected value.
let address: String = row.get(2)?;
let decoded = RecipientAddress::decode(&self.params, &address).ok_or_else(|| {
WalletMigrationError::CorruptedData(format!(
"Could not decode {} as a valid Zcash address.",
address
))
})?;
let decoded_address = if let RecipientAddress::Unified(ua) = decoded {
ua
} else {
return Err(WalletMigrationError::CorruptedData(
"Address in accounts table was not a Unified Address.".to_string(),
));
};
let (expected_address, idx) = ufvk.default_address();
if decoded_address != expected_address {
return Err(WalletMigrationError::CorruptedData(format!(
"Decoded UA {} does not match the UFVK's default address {} at {:?}.",
address,
RecipientAddress::Unified(expected_address).encode(&self.params),
idx,
)));
}
// The transparent_address column might not be filled, depending on how this
// crate was compiled.
if let Some(transparent_address) = row.get::<_, Option<String>>(3)? {
let decoded_transparent =
RecipientAddress::decode(&self.params, &transparent_address).ok_or_else(
|| {
WalletMigrationError::CorruptedData(format!(
"Could not decode {} as a valid Zcash address.",
address
))
},
)?;
let decoded_transparent_address = if let RecipientAddress::Transparent(addr) =
decoded_transparent
{
addr
} else {
return Err(WalletMigrationError::CorruptedData(
"Address in transparent_address column of accounts table was not a transparent address.".to_string(),
));
};
// Verify that the transparent_address column contains the expected value,
// so we can confidently delete the column knowing we can regenerate the
// values from the stored UFVKs.
// We can only check if it is the expected transparent address if the
// transparent-inputs feature flag is enabled.
#[cfg(feature = "transparent-inputs")]
{
let expected_address = ufvk
.transparent()
.and_then(|k| k.derive_external_ivk().ok().map(|k| k.default_address().0));
if Some(decoded_transparent_address) != expected_address {
return Err(WalletMigrationError::CorruptedData(format!(
"Decoded transparent address {} is not the default transparent address.",
transparent_address,
)));
}
}
// If the transparent_address column is not empty, and we can't check its
// value, return an error.
#[cfg(not(feature = "transparent-inputs"))]
{
let _ = decoded_transparent_address;
return Err(WalletMigrationError::CorruptedData(
"Database needs transparent-inputs feature flag enabled to migrate"
.to_string(),
));
}
}
add_account_internal::<P, WalletMigrationError>(
&self.params,
transaction,
"accounts_new",
account,
&ufvk,
)?;
}
transaction.execute_batch(
"DROP TABLE accounts;
ALTER TABLE accounts_new RENAME TO accounts;",
)?;
Ok(())
}
fn down(&self, _transaction: &Transaction) -> Result<(), WalletMigrationError> {
// TODO: something better than just panic?
panic!("Cannot revert this migration.");
}
}

View File

@ -7,7 +7,9 @@ and this library adheres to Rust's notion of
## [Unreleased] ## [Unreleased]
### Added ### Added
- `zcash_primitives::legacy::AccountPrivKey::{to_bytes, from_bytes}` - `zcash_primitives::legacy`:
- `impl {Copy, Eq, Ord} for TransparentAddress`
- `keys::AccountPrivKey::{to_bytes, from_bytes}`
- `zcash_primitives::sapling::NullifierDerivingKey` - `zcash_primitives::sapling::NullifierDerivingKey`
- Added in `zcash_primitives::sapling::keys` - Added in `zcash_primitives::sapling::keys`
- `DecodingError` - `DecodingError`

View File

@ -95,7 +95,7 @@ impl Shl<&[u8]> for Script {
} }
/// A transparent address corresponding to either a public key or a `Script`. /// A transparent address corresponding to either a public key or a `Script`.
#[derive(Debug, PartialEq, PartialOrd, Hash, Clone)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TransparentAddress { pub enum TransparentAddress {
PublicKey([u8; 20]), // TODO: Rename to PublicKeyHash PublicKey([u8; 20]), // TODO: Rename to PublicKeyHash
Script([u8; 20]), // TODO: Rename to ScriptHash Script([u8; 20]), // TODO: Rename to ScriptHash