Merge pull request #632 from nuttycom/data_api/get_next_address

Replace `get_address` with `get_current_address` and `get_next_available_address`
This commit is contained in:
Kris Nuttycombe 2022-09-12 18:43:38 -06:00 committed by GitHub
commit b5908dc964
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 179 additions and 40 deletions

View File

@ -32,8 +32,10 @@ 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_current_address`
- `WalletRead::get_all_nullifiers` - `WalletRead::get_all_nullifiers`
- `WalletWrite::remove_unmined_tx` (behind the `unstable` feature flag). - `WalletWrite::remove_unmined_tx` (behind the `unstable` feature flag).
- `WalletWrite::get_next_available_address`
- `zcash_client_backend::proto`: - `zcash_client_backend::proto`:
- `actions` field on `compact_formats::CompactTx` - `actions` field on `compact_formats::CompactTx`
- `compact_formats::CompactOrchardAction` - `compact_formats::CompactOrchardAction`
@ -79,8 +81,6 @@ 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
@ -128,6 +128,8 @@ and this library adheres to Rust's notion of
- `zcash_client_backend::data_api`: - `zcash_client_backend::data_api`:
- `WalletRead::get_extended_full_viewing_keys` (use - `WalletRead::get_extended_full_viewing_keys` (use
`WalletRead::get_unified_full_viewing_keys` instead). `WalletRead::get_unified_full_viewing_keys` instead).
- `WalletRead::get_address` (use `WalletRead::get_current_address` or
`WalletWrite::get_next_available_address` instead.)
- The hardcoded `data_api::wallet::ANCHOR_OFFSET` constant. - The hardcoded `data_api::wallet::ANCHOR_OFFSET` constant.
- `zcash_client_backend::wallet::AccountId` (moved to `zcash_primitives::zip32::AccountId`). - `zcash_client_backend::wallet::AccountId` (moved to `zcash_primitives::zip32::AccountId`).

View File

@ -114,12 +114,15 @@ 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 unified address for the specified account, if the account /// Returns the most recently generated unified address for the specified account, if the
/// identifier specified refers to a valid account for this wallet. /// account 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.
fn get_address(&self, account: AccountId) -> Result<Option<UnifiedAddress>, Self::Error>; fn get_current_address(
&self,
account: AccountId,
) -> Result<Option<UnifiedAddress>, 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(
@ -266,6 +269,16 @@ pub struct SentTransactionOutput<'a> {
/// This trait encapsulates the write capabilities required to update stored /// This trait encapsulates the write capabilities required to update stored
/// wallet data. /// wallet data.
pub trait WalletWrite: WalletRead { pub trait WalletWrite: WalletRead {
/// Generates and persists the next available diversified address, given the current
/// addresses known to the wallet.
///
/// Returns `Ok(None)` if the account identifier does not correspond to a known
/// account.
fn get_next_available_address(
&mut self,
account: AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error>;
/// Updates the state of the wallet database by persisting the provided /// Updates the state of the wallet database by persisting the provided
/// block information, along with the updated witness data that was /// block information, along with the updated witness data that was
/// produced when scanning the block for transactions pertaining to /// produced when scanning the block for transactions pertaining to
@ -283,6 +296,8 @@ pub trait WalletWrite: WalletRead {
received_tx: &DecryptedTransaction, received_tx: &DecryptedTransaction,
) -> Result<Self::TxRef, Self::Error>; ) -> Result<Self::TxRef, Self::Error>;
/// Saves information about a transaction that was constructed and sent by the wallet to the
/// persistent wallet store.
fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<Self::TxRef, Self::Error>; fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<Self::TxRef, Self::Error>;
/// Removes the specified unmined transaction from the persistent wallet store, if it /// Removes the specified unmined transaction from the persistent wallet store, if it
@ -409,7 +424,10 @@ pub mod testing {
Ok(None) Ok(None)
} }
fn get_address(&self, _account: AccountId) -> Result<Option<UnifiedAddress>, Self::Error> { fn get_current_address(
&self,
_account: AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
Ok(None) Ok(None)
} }
@ -503,6 +521,13 @@ pub mod testing {
} }
impl WalletWrite for MockWalletDb { impl WalletWrite for MockWalletDb {
fn get_next_available_address(
&mut self,
_account: AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
Ok(None)
}
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn advance_by_block( fn advance_by_block(
&mut self, &mut self,

View File

@ -16,6 +16,8 @@ and this library adheres to Rust's notion of
transparent address decoding. transparent address decoding.
- `SqliteClientError::RequestedRewindInvalid`, to report when requested - `SqliteClientError::RequestedRewindInvalid`, to report when requested
rewinds exceed supported bounds. rewinds exceed supported bounds.
- `SqliteClientError::DiversifierIndexOutOfRange`, to report when the space
of available diversifier indices has been exhausted.
- An `unstable` feature flag; this is added to parts of the API that may change - An `unstable` feature flag; this is added to parts of the API that may change
in any release. It enables `zcash_client_backend`'s `unstable` feature flag. in any release. It enables `zcash_client_backend`'s `unstable` feature flag.
- New summary views that may be directly accessed in the sqlite database. - New summary views that may be directly accessed in the sqlite database.

View File

@ -65,6 +65,10 @@ pub enum SqliteClientError {
/// Wrapper for errors from zcash_client_backend /// Wrapper for errors from zcash_client_backend
BackendError(data_api::error::Error<NoteId>), BackendError(data_api::error::Error<NoteId>),
/// The space of allocatable diversifier indices has been exhausted for
/// the given account.
DiversifierIndexOutOfRange,
} }
impl error::Error for SqliteClientError { impl error::Error for SqliteClientError {
@ -103,6 +107,7 @@ impl fmt::Display for SqliteClientError {
SqliteClientError::Io(e) => write!(f, "{}", e), SqliteClientError::Io(e) => write!(f, "{}", e),
SqliteClientError::InvalidMemo(e) => write!(f, "{}", e), SqliteClientError::InvalidMemo(e) => write!(f, "{}", e),
SqliteClientError::BackendError(e) => write!(f, "{}", e), SqliteClientError::BackendError(e) => write!(f, "{}", e),
SqliteClientError::DiversifierIndexOutOfRange => write!(f, "The space of available diversifier indices is exhausted"),
} }
} }
} }

View File

@ -48,7 +48,7 @@ use zcash_primitives::{
merkle_tree::{CommitmentTree, IncrementalWitness}, merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{Node, Nullifier}, sapling::{Node, Nullifier},
transaction::{components::Amount, Transaction, TxId}, transaction::{components::Amount, Transaction, TxId},
zip32::{AccountId, ExtendedFullViewingKey}, zip32::{AccountId, DiversifierIndex, ExtendedFullViewingKey},
}; };
use zcash_client_backend::{ use zcash_client_backend::{
@ -153,8 +153,11 @@ 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<UnifiedAddress>, Self::Error> { fn get_current_address(
wallet::get_address_ua(self, account) &self,
account: AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
wallet::get_current_address(self, account).map(|res| res.map(|(addr, _)| addr))
} }
fn is_valid_account_extfvk( fn is_valid_account_extfvk(
@ -276,8 +279,11 @@ 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<UnifiedAddress>, Self::Error> { fn get_current_address(
self.wallet_db.get_address(account) &self,
account: AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
self.wallet_db.get_current_address(account)
} }
fn is_valid_account_extfvk( fn is_valid_account_extfvk(
@ -396,6 +402,34 @@ impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
#[allow(deprecated)] #[allow(deprecated)]
impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> { impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
fn get_next_available_address(
&mut self,
account: AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
match self.get_unified_full_viewing_keys()?.get(&account) {
Some(ufvk) => {
let search_from = match wallet::get_current_address(self.wallet_db, account)? {
Some((_, mut last_diversifier_index)) => {
last_diversifier_index
.increment()
.map_err(|_| SqliteClientError::DiversifierIndexOutOfRange)?;
last_diversifier_index
}
None => DiversifierIndex::default(),
};
let (addr, diversifier_index) = ufvk
.find_address(search_from)
.ok_or(SqliteClientError::DiversifierIndexOutOfRange)?;
self.stmt_insert_address(account, diversifier_index, &addr)?;
Ok(Some(addr))
}
None => Ok(None),
}
}
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
fn advance_by_block( fn advance_by_block(
&mut self, &mut self,
@ -691,13 +725,6 @@ mod tests {
use rusqlite::params; use rusqlite::params;
use std::collections::HashMap; use std::collections::HashMap;
use zcash_client_backend::{
keys::{sapling, UnifiedFullViewingKey},
proto::compact_formats::{
CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
},
};
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use zcash_primitives::{legacy, legacy::keys::IncomingViewingKey}; use zcash_primitives::{legacy, legacy::keys::IncomingViewingKey};
@ -714,7 +741,18 @@ mod tests {
zip32::ExtendedFullViewingKey, zip32::ExtendedFullViewingKey,
}; };
use crate::{wallet::init::init_accounts_table, AccountId, WalletDb}; use zcash_client_backend::{
data_api::{WalletRead, WalletWrite},
keys::{sapling, UnifiedFullViewingKey},
proto::compact_formats::{
CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx,
},
};
use crate::{
wallet::init::{init_accounts_table, init_wallet_db},
AccountId, WalletDb,
};
use super::BlockDb; use super::BlockDb;
@ -930,6 +968,29 @@ mod tests {
.unwrap(); .unwrap();
} }
#[test]
pub(crate) fn get_next_available_address() {
use tempfile::NamedTempFile;
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network()).unwrap();
let account = AccountId::from(0);
init_wallet_db(&mut db_data, None).unwrap();
let _ = init_test_accounts_table_ufvk(&db_data);
let current_addr = db_data.get_current_address(account).unwrap();
assert!(current_addr.is_some());
let mut update_ops = db_data.get_update_ops().unwrap();
let addr2 = update_ops.get_next_available_address(account).unwrap();
assert!(addr2.is_some());
assert_ne!(current_addr, addr2);
let addr2_cur = db_data.get_current_address(account).unwrap();
assert_eq!(addr2, addr2_cur);
}
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
#[test] #[test]
fn transparent_receivers() { fn transparent_receivers() {

View File

@ -15,9 +15,11 @@ use zcash_primitives::{
merkle_tree::{CommitmentTree, IncrementalWitness}, merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{Diversifier, Node, Nullifier}, sapling::{Diversifier, Node, Nullifier},
transaction::{components::Amount, TxId}, transaction::{components::Amount, TxId},
zip32::AccountId, zip32::{AccountId, DiversifierIndex},
}; };
use zcash_client_backend::address::UnifiedAddress;
use crate::{error::SqliteClientError, wallet::PoolType, NoteId, WalletDb}; use crate::{error::SqliteClientError, wallet::PoolType, NoteId, WalletDb};
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
@ -64,6 +66,8 @@ pub struct DataConnStmtCache<'a, P> {
stmt_insert_witness: Statement<'a>, stmt_insert_witness: Statement<'a>,
stmt_prune_witnesses: Statement<'a>, stmt_prune_witnesses: Statement<'a>,
stmt_update_expired: Statement<'a>, stmt_update_expired: Statement<'a>,
stmt_insert_address: Statement<'a>,
} }
impl<'a, P> DataConnStmtCache<'a, P> { impl<'a, P> DataConnStmtCache<'a, P> {
@ -160,6 +164,10 @@ impl<'a, P> DataConnStmtCache<'a, P> {
WHERE id_tx = received_notes.spent AND block IS NULL AND expiry_height < ? WHERE id_tx = received_notes.spent AND block IS NULL AND expiry_height < ?
)", )",
)?, )?,
stmt_insert_address: wallet_db.conn.prepare(
"INSERT INTO addresses (account, diversifier_index_be, address)
VALUES (:account, :diversifier_index_be, :address)",
)?,
} }
) )
} }
@ -369,6 +377,27 @@ impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
Ok(rows) Ok(rows)
} }
/// Adds the given address and diversifier index to the addresses table.
///
/// Returns the database row for the newly-inserted address.
pub(crate) fn stmt_insert_address(
&mut self,
account: AccountId,
mut diversifier_index: DiversifierIndex,
address: &UnifiedAddress,
) -> Result<i64, SqliteClientError> {
diversifier_index.0.reverse();
let sql_args: &[(&str, &dyn ToSql)] = &[
(":account", &u32::from(account)),
(":diversifier_index_be", &&diversifier_index.0[..]),
(":address", &address.encode(&self.wallet_db.params)),
];
self.stmt_insert_address.execute_named(sql_args)?;
Ok(self.wallet_db.conn.last_insert_rowid())
}
} }
impl<'a, P> DataConnStmtCache<'a, P> { impl<'a, P> DataConnStmtCache<'a, P> {

View File

@ -22,7 +22,7 @@ use zcash_primitives::{
merkle_tree::{CommitmentTree, IncrementalWitness}, merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::{keys::DiversifiableFullViewingKey, Node, Note, Nullifier, PaymentAddress}, sapling::{keys::DiversifiableFullViewingKey, Node, Note, Nullifier, PaymentAddress},
transaction::{components::Amount, Transaction, TxId}, transaction::{components::Amount, Transaction, TxId},
zip32::{AccountId, ExtendedFullViewingKey}, zip32::{AccountId, DiversifierIndex, ExtendedFullViewingKey},
}; };
use zcash_client_backend::{ use zcash_client_backend::{
@ -158,10 +158,12 @@ 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. // This returns the most recently generated address.
let addr: String = wdb.conn.query_row( let addr: String = wdb.conn.query_row(
"SELECT address FROM addresses "SELECT address
WHERE account = ?", FROM addresses WHERE account = ?
ORDER BY diversifier_index_be DESC
LIMIT 1",
&[u32::from(account)], &[u32::from(account)],
|row| row.get(0), |row| row.get(0),
)?; )?;
@ -177,21 +179,31 @@ pub fn get_address<P: consensus::Parameters>(
}) })
} }
pub(crate) fn get_address_ua<P: consensus::Parameters>( pub(crate) fn get_current_address<P: consensus::Parameters>(
wdb: &WalletDb<P>, wdb: &WalletDb<P>,
account: AccountId, account: AccountId,
) -> Result<Option<UnifiedAddress>, SqliteClientError> { ) -> Result<Option<(UnifiedAddress, DiversifierIndex)>, SqliteClientError> {
// This returns the first diversified address, which will be the default one. // This returns the most recently generated address.
let addr: Option<String> = wdb let addr: Option<(String, Vec<u8>)> = wdb
.conn .conn
.query_row_named( .query_row_named(
"SELECT address FROM addresses WHERE account = :account", "SELECT address, diversifier_index_be
FROM addresses WHERE account = :account
ORDER BY diversifier_index_be DESC
LIMIT 1",
&[(":account", &u32::from(account))], &[(":account", &u32::from(account))],
|row| row.get(0), |row| Ok((row.get(0)?, row.get(1)?)),
) )
.optional()?; .optional()?;
addr.map(|addr_str| { addr.map(|(addr_str, di_vec)| {
let mut di_be: [u8; 11] = di_vec.try_into().map_err(|_| {
SqliteClientError::CorruptedData(
"Diverisifier index is not an 11-byte value".to_owned(),
)
})?;
di_be.reverse();
RecipientAddress::decode(&wdb.params, &addr_str) RecipientAddress::decode(&wdb.params, &addr_str)
.ok_or_else(|| { .ok_or_else(|| {
SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned()) SqliteClientError::CorruptedData("Not a valid Zcash recipient address".to_owned())
@ -203,6 +215,7 @@ pub(crate) fn get_address_ua<P: consensus::Parameters>(
addr_str, addr_str,
))), ))),
}) })
.map(|addr| (addr, DiversifierIndex(di_be)))
}) })
.transpose() .transpose()
} }
@ -619,7 +632,7 @@ pub fn get_block_hash<P>(
pub fn get_rewind_height<P>(wdb: &WalletDb<P>) -> Result<Option<BlockHeight>, SqliteClientError> { pub fn get_rewind_height<P>(wdb: &WalletDb<P>) -> Result<Option<BlockHeight>, SqliteClientError> {
wdb.conn wdb.conn
.query_row( .query_row(
"SELECT MIN(tx.block) "SELECT MIN(tx.block)
FROM received_notes n FROM received_notes n
JOIN transactions tx ON tx.id_tx = n.tx JOIN transactions tx ON tx.id_tx = n.tx
WHERE n.spent IS NULL", WHERE n.spent IS NULL",

View File

@ -608,14 +608,16 @@ fn add_account_internal<P: consensus::Parameters, E: From<rusqlite::Error>>(
)?; )?;
// Always derive the default Unified Address for the account. // Always derive the default Unified Address for the account.
let (address, idx) = key.default_address(); let (address, mut idx) = key.default_address();
let address_str: String = address.encode(network); let address_str: String = address.encode(network);
// the diversifier index is stored in big-endian order to allow sorting
idx.0.reverse();
conn.execute_named( conn.execute_named(
"INSERT INTO addresses (account, diversifier_index, address) "INSERT INTO addresses (account, diversifier_index_be, address)
VALUES (:account, :diversifier_index, :address)", VALUES (:account, :diversifier_index_be, :address)",
&[ &[
(":account", &<u32>::from(account)), (":account", &<u32>::from(account)),
(":diversifier_index", &&idx.0[..]), (":diversifier_index_be", &&idx.0[..]),
(":address", &address_str), (":address", &address_str),
], ],
)?; )?;
@ -730,10 +732,10 @@ mod tests {
)", )",
"CREATE TABLE addresses ( "CREATE TABLE addresses (
account INTEGER NOT NULL, account INTEGER NOT NULL,
diversifier_index BLOB NOT NULL, diversifier_index_be BLOB NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
FOREIGN KEY (account) REFERENCES accounts(account), FOREIGN KEY (account) REFERENCES accounts(account),
CONSTRAINT diversification UNIQUE (account, diversifier_index) CONSTRAINT diversification UNIQUE (account, diversifier_index_be)
)", )",
"CREATE TABLE blocks ( "CREATE TABLE blocks (
height INTEGER PRIMARY KEY, height INTEGER PRIMARY KEY,

View File

@ -51,10 +51,10 @@ impl<P: consensus::Parameters> RusqliteMigration for AddressesTableMigration<P>
transaction.execute_batch( transaction.execute_batch(
"CREATE TABLE addresses ( "CREATE TABLE addresses (
account INTEGER NOT NULL, account INTEGER NOT NULL,
diversifier_index BLOB NOT NULL, diversifier_index_be BLOB NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
FOREIGN KEY (account) REFERENCES accounts(account), FOREIGN KEY (account) REFERENCES accounts(account),
CONSTRAINT diversification UNIQUE (account, diversifier_index) CONSTRAINT diversification UNIQUE (account, diversifier_index_be)
); );
CREATE TABLE accounts_new ( CREATE TABLE accounts_new (
account INTEGER PRIMARY KEY, account INTEGER PRIMARY KEY,