WalletRead for WalletDb {
wallet::get_unified_full_viewing_keys(self)
}
- fn get_address(&self, account: AccountId) -> Result, Self::Error> {
- #[allow(deprecated)]
- wallet::get_address(self, account)
+ fn get_address(&self, account: AccountId) -> Result , Self::Error> {
+ wallet::get_address_ua(self, account)
}
fn is_valid_account_extfvk(
@@ -235,6 +237,13 @@ impl WalletRead for WalletDb {
#[cfg(feature = "transparent-inputs")]
impl WalletReadTransparent for WalletDb {
+ fn get_transparent_receivers(
+ &self,
+ account: AccountId,
+ ) -> Result, Self::Error> {
+ wallet::get_transparent_receivers(self, account)
+ }
+
fn get_unspent_transparent_outputs(
&self,
address: &TransparentAddress,
@@ -267,7 +276,7 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
self.wallet_db.get_unified_full_viewing_keys()
}
- fn get_address(&self, account: AccountId) -> Result, Self::Error> {
+ fn get_address(&self, account: AccountId) -> Result , Self::Error> {
self.wallet_db.get_address(account)
}
@@ -340,6 +349,13 @@ impl<'a, P: consensus::Parameters> WalletRead for DataConnStmtCache<'a, P> {
#[cfg(feature = "transparent-inputs")]
impl<'a, P: consensus::Parameters> WalletReadTransparent for DataConnStmtCache<'a, P> {
+ fn get_transparent_receivers(
+ &self,
+ account: AccountId,
+ ) -> Result, Self::Error> {
+ self.wallet_db.get_transparent_receivers(account)
+ }
+
fn get_unspent_transparent_outputs(
&self,
address: &TransparentAddress,
@@ -725,6 +741,14 @@ mod tests {
pub(crate) fn init_test_accounts_table(
db_data: &WalletDb,
) -> (DiversifiableFullViewingKey, Option) {
+ 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,
+ ) -> (UnifiedFullViewingKey, Option) {
let seed = [0u8; 32];
let account = AccountId::from(0);
let extsk = sapling::spending_key(&seed, network().coin_type(), account);
@@ -745,15 +769,15 @@ mod tests {
let ufvk = UnifiedFullViewingKey::new(
#[cfg(feature = "transparent-inputs")]
tkey,
- Some(dfvk.clone()),
+ Some(dfvk),
None,
)
.unwrap();
- let ufvks = HashMap::from([(account, ufvk)]);
+ let ufvks = HashMap::from([(account, ufvk.clone())]);
init_accounts_table(db_data, &ufvks).unwrap();
- (dfvk, taddr)
+ (ufvk, taddr)
}
/// Create a fake CompactBlock at the given height, containing a single output paying
@@ -901,6 +925,36 @@ mod tests {
.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")]
#[test]
fn remove_unmined_tx_reverts_balance() {
diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs
index 7f65267f6..709251dbc 100644
--- a/zcash_client_sqlite/src/wallet.rs
+++ b/zcash_client_sqlite/src/wallet.rs
@@ -12,6 +12,9 @@ use rusqlite::{OptionalExtension, ToSql, NO_PARAMS};
use std::collections::HashMap;
use std::convert::TryFrom;
+#[cfg(feature = "transparent-inputs")]
+use std::collections::HashSet;
+
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters},
@@ -23,7 +26,7 @@ use zcash_primitives::{
};
use zcash_client_backend::{
- address::RecipientAddress,
+ address::{RecipientAddress, UnifiedAddress},
data_api::error::Error,
encoding::{encode_payment_address_p, encode_transparent_address_p},
keys::UnifiedFullViewingKey,
@@ -41,7 +44,7 @@ use {
rusqlite::params,
zcash_client_backend::{encoding::AddressCodec, wallet::WalletTransparentOutput},
zcash_primitives::{
- legacy::Script,
+ legacy::{keys::IncomingViewingKey, Script},
transaction::components::{OutPoint, TxOut},
},
};
@@ -155,8 +158,9 @@ pub fn get_address(
wdb: &WalletDb,
account: AccountId,
) -> Result, SqliteClientError> {
+ // This returns the first diversified address, which will be the default one.
let addr: String = wdb.conn.query_row(
- "SELECT address FROM accounts
+ "SELECT address FROM addresses
WHERE account = ?",
&[u32::from(account)],
|row| row.get(0),
@@ -173,6 +177,86 @@ pub fn get_address(
})
}
+pub(crate) fn get_address_ua(
+ wdb: &WalletDb,
+ account: AccountId,
+) -> Result, SqliteClientError> {
+ // This returns the first diversified address, which will be the default one.
+ let addr: Option = 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(
+ wdb: &WalletDb,
+ account: AccountId,
+) -> Result, 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.
pub(crate) fn get_unified_full_viewing_keys(
wdb: &WalletDb,
diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs
index 5dc3c11af..f0de43e69 100644
--- a/zcash_client_sqlite/src/wallet/init.rs
+++ b/zcash_client_sqlite/src/wallet/init.rs
@@ -26,6 +26,8 @@ use {
zcash_primitives::legacy::keys::IncomingViewingKey,
};
+mod migrations;
+
#[derive(Debug)]
pub enum WalletMigrationError {
/// The seed is required for the migration.
@@ -289,12 +291,34 @@ impl RusqliteMigration for WalletMigration2 {
}
}
- add_account_internal::
(
- &self.params,
- transaction,
- "accounts_new",
- account,
- &ufvk,
+ let ufvk_str: String = ufvk.encode(&self.params);
+ let address_str: String = ufvk.default_address().0.encode(&self.params);
+
+ // This migration, and the wallet behaviour before it, stored the default
+ // transparent address in the `accounts` table. This does not necessarily
+ // 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 = 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 = None;
+
+ transaction.execute_named(
+ "INSERT INTO accounts_new (account, ufvk, address, transparent_address)
+ VALUES (:account, :ufvk, :address, :transparent_address)",
+ &[
+ (":account", &::from(account)),
+ (":ufvk", &ufvk_str),
+ (":address", &address_str),
+ (":transparent_address", &taddress_str),
+ ],
)?;
} else {
return Err(WalletMigrationError::SeedRequired);
@@ -463,9 +487,12 @@ pub fn init_wallet_db(
params: wdb.params.clone(),
seed,
});
+ let addrs_migration = Box::new(migrations::AddressesTableMigration {
+ params: wdb.params.clone(),
+ });
migrator
- .register_multiple(vec![migration0, migration1, migration2])
+ .register_multiple(vec![migration0, migration1, migration2, addrs_migration])
.expect("Wallet migration registration should have been successful.");
migrator.up(None)?;
wdb.conn
@@ -557,23 +584,25 @@ fn add_account_internal>(
key: &UnifiedFullViewingKey,
) -> Result<(), E> {
let ufvk_str: String = key.encode(network);
- let address_str: String = key.default_address().0.encode(network);
- #[cfg(feature = "transparent-inputs")]
- let taddress_str: Option = 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 = None;
-
- conn.execute(
+ conn.execute_named(
&format!(
- "INSERT INTO {} (account, ufvk, address, transparent_address)
- VALUES (?, ?, ?, ?)",
+ "INSERT INTO {} (account, ufvk) VALUES (:account, :ufvk)",
accounts_table
),
- params![::from(account), ufvk_str, address_str, taddress_str],
+ &[(":account", &::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", &::from(account)),
+ (":diversifier_index", &&idx.0[..]),
+ (":address", &address_str),
+ ],
)?;
Ok(())
@@ -683,9 +712,14 @@ mod tests {
let expected = vec![
"CREATE TABLE \"accounts\" (
account INTEGER PRIMARY KEY,
- ufvk TEXT NOT NULL,
- address TEXT,
- transparent_address TEXT
+ ufvk TEXT NOT NULL
+ )",
+ "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 (
height INTEGER PRIMARY KEY,
@@ -1148,10 +1182,9 @@ mod tests {
// add a transparent "sent note"
#[cfg(feature = "transparent-inputs")]
{
- let taddr = RecipientAddress::Transparent(
- ufvk.default_address().0.transparent().unwrap().clone(),
- )
- .encode(&tests::network());
+ let taddr =
+ RecipientAddress::Transparent(*ufvk.default_address().0.transparent().unwrap())
+ .encode(&tests::network());
wdb.conn.execute(
"INSERT INTO blocks (height, hash, time, sapling_tree) VALUES (0, 0, 0, '')",
NO_PARAMS,
diff --git a/zcash_client_sqlite/src/wallet/init/migrations.rs b/zcash_client_sqlite/src/wallet/init/migrations.rs
new file mode 100644
index 000000000..deea2236c
--- /dev/null
+++ b/zcash_client_sqlite/src/wallet/init/migrations.rs
@@ -0,0 +1,2 @@
+mod addresses_table;
+pub(super) use addresses_table::AddressesTableMigration;
diff --git a/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs
new file mode 100644
index 000000000..a627b5f26
--- /dev/null
+++ b/zcash_client_sqlite/src/wallet/init/migrations/addresses_table.rs
@@ -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 {
+ pub(crate) params: P,
+}
+
+impl Migration for AddressesTableMigration {
+ fn id(&self) -> Uuid {
+ ADDRESSES_TABLE_MIGRATION
+ }
+
+ fn dependencies(&self) -> HashSet {
+ ["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 RusqliteMigration for AddressesTableMigration {
+ 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>(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::(
+ &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.");
+ }
+}
diff --git a/zcash_primitives/CHANGELOG.md b/zcash_primitives/CHANGELOG.md
index c528e685e..75d1dbdd0 100644
--- a/zcash_primitives/CHANGELOG.md
+++ b/zcash_primitives/CHANGELOG.md
@@ -7,7 +7,9 @@ and this library adheres to Rust's notion of
## [Unreleased]
### 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`
- Added in `zcash_primitives::sapling::keys`
- `DecodingError`
diff --git a/zcash_primitives/src/legacy.rs b/zcash_primitives/src/legacy.rs
index f768cda62..933cbff45 100644
--- a/zcash_primitives/src/legacy.rs
+++ b/zcash_primitives/src/legacy.rs
@@ -95,7 +95,7 @@ impl Shl<&[u8]> for 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 {
PublicKey([u8; 20]), // TODO: Rename to PublicKeyHash
Script([u8; 20]), // TODO: Rename to ScriptHash