Add WalletWrite::create_account function

This commit is contained in:
Kris Nuttycombe 2022-09-13 16:43:04 -06:00
parent b5908dc964
commit d0062a87d4
11 changed files with 143 additions and 57 deletions

View File

@ -34,6 +34,7 @@ and this library adheres to Rust's notion of
- `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::proto`:

View File

@ -35,6 +35,7 @@ rand_core = "0.6"
rayon = "1.5"
ripemd = { version = "0.1", optional = true }
secp256k1 = { version = "0.21", optional = true }
secrecy = "0.8"
sha2 = { version = "0.10.1", optional = true }
subtle = "2.2.3"
time = "0.2"

View File

@ -1,5 +1,6 @@
//! Interfaces for wallet data persistence & low-level wallet utilities.
use secrecy::SecretVec;
use std::cmp;
use std::collections::HashMap;
use std::fmt::Debug;
@ -20,7 +21,7 @@ use zcash_primitives::{
use crate::{
address::{RecipientAddress, UnifiedAddress},
decrypt::DecryptedOutput,
keys::UnifiedFullViewingKey,
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock,
wallet::{SpendableNote, WalletTx},
};
@ -269,6 +270,14 @@ pub struct SentTransactionOutput<'a> {
/// This trait encapsulates the write capabilities required to update stored
/// wallet data.
pub trait WalletWrite: WalletRead {
/// Creates a new account-level spending authority by derivation from the provided
/// seed using the next available unused account identifier, and returns this identifier
/// along with the generated unified spending key.
fn create_account(
&mut self,
seed: SecretVec<u8>,
) -> Result<(AccountId, UnifiedSpendingKey), Self::Error>;
/// Generates and persists the next available diversified address, given the current
/// addresses known to the wallet.
///
@ -353,6 +362,7 @@ pub trait BlockSource {
#[cfg(feature = "test-dependencies")]
pub mod testing {
use secrecy::{ExposeSecret, SecretVec};
use std::collections::HashMap;
#[cfg(feature = "transparent-inputs")]
@ -360,7 +370,7 @@ pub mod testing {
use zcash_primitives::{
block::BlockHash,
consensus::BlockHeight,
consensus::{BlockHeight, Network},
legacy::TransparentAddress,
memo::Memo,
merkle_tree::{CommitmentTree, IncrementalWitness},
@ -371,7 +381,7 @@ pub mod testing {
use crate::{
address::UnifiedAddress,
keys::UnifiedFullViewingKey,
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock,
wallet::{SpendableNote, WalletTransparentOutput},
};
@ -402,7 +412,9 @@ pub mod testing {
}
}
pub struct MockWalletDb {}
pub struct MockWalletDb {
pub network: Network,
}
impl WalletRead for MockWalletDb {
type Error = Error<u32>;
@ -521,6 +533,16 @@ pub mod testing {
}
impl WalletWrite for MockWalletDb {
fn create_account(
&mut self,
seed: SecretVec<u8>,
) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> {
let account = AccountId::from(0);
UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account)
.map(|k| (account, k))
.map_err(|_| Error::KeyDerivationError(account))
}
fn get_next_available_address(
&mut self,
_account: AccountId,

View File

@ -29,7 +29,9 @@
//! # fn test() -> Result<(), Error<u32>> {
//! let network = Network::TestNetwork;
//! let db_cache = testing::MockBlockSource {};
//! let mut db_data = testing::MockWalletDb {};
//! let mut db_data = testing::MockWalletDb {
//! network: Network::TestNetwork
//! };
//!
//! // 1) Download new CompactBlocks into db_cache.
//!

View File

@ -71,6 +71,10 @@ pub enum Error<NoteId> {
/// It is forbidden to provide a memo when constructing a transparent output.
MemoForbidden,
/// An error occurred deriving a spending key from a seed and an account
/// identifier.
KeyDerivationError(AccountId),
}
impl ChainInvalid {
@ -122,6 +126,7 @@ impl<N: fmt::Display> fmt::Display for Error<N> {
Error::Protobuf(e) => write!(f, "{}", e),
Error::SaplingNotActive => write!(f, "Could not determine Sapling upgrade activation height."),
Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."),
Error::KeyDerivationError(acct_id) => write!(f, "Key derivation failed for account {:?}", acct_id),
}
}
}

View File

@ -149,7 +149,9 @@ where
/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, account);
/// let to = extsk.default_address().1.into();
///
/// let mut db_read = testing::MockWalletDb {};
/// let mut db_read = testing::MockWalletDb {
/// network: Network::TestNetwork
/// };
///
/// create_spend_to_address(
/// &mut db_read,

View File

@ -7,7 +7,7 @@ use zcash_client_backend::{
data_api,
encoding::{Bech32DecodeError, TransparentCodecError},
};
use zcash_primitives::consensus::BlockHeight;
use zcash_primitives::{consensus::BlockHeight, zip32::AccountId};
use crate::{NoteId, PRUNING_HEIGHT};
@ -69,6 +69,10 @@ pub enum SqliteClientError {
/// The space of allocatable diversifier indices has been exhausted for
/// the given account.
DiversifierIndexOutOfRange,
/// An error occurred deriving a spending key from a seed and an account
/// identifier.
KeyDerivationError(AccountId),
}
impl error::Error for SqliteClientError {
@ -108,6 +112,7 @@ impl fmt::Display for SqliteClientError {
SqliteClientError::InvalidMemo(e) => write!(f, "{}", e),
SqliteClientError::BackendError(e) => write!(f, "{}", e),
SqliteClientError::DiversifierIndexOutOfRange => write!(f, "The space of available diversifier indices is exhausted"),
SqliteClientError::KeyDerivationError(acct_id) => write!(f, "Key derivation failed for account {:?}", acct_id),
}
}
}

View File

@ -32,6 +32,7 @@
// Catch documentation errors caused by code changes.
#![deny(rustdoc::broken_intra_doc_links)]
use secrecy::{ExposeSecret, SecretVec};
use std::collections::HashMap;
use std::fmt;
use std::path::Path;
@ -56,7 +57,7 @@ use zcash_client_backend::{
data_api::{
BlockSource, DecryptedTransaction, PrunedBlock, SentTransaction, WalletRead, WalletWrite,
},
keys::UnifiedFullViewingKey,
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock,
wallet::SpendableNote,
};
@ -402,6 +403,29 @@ impl<'a, P: consensus::Parameters> DataConnStmtCache<'a, P> {
#[allow(deprecated)]
impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
fn create_account(
&mut self,
seed: SecretVec<u8>,
) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> {
self.transactionally(|stmts| {
let account = wallet::get_max_account_id(stmts.wallet_db)?
.map(|a| AccountId::from(u32::from(a) + 1))
.unwrap_or_else(|| AccountId::from(0));
let usk = UnifiedSpendingKey::from_seed(
&stmts.wallet_db.params,
seed.expose_secret(),
account,
)
.map_err(|_| SqliteClientError::KeyDerivationError(account))?;
let ufvk = usk.to_unified_full_viewing_key();
wallet::add_account(stmts.wallet_db, account, &ufvk)?;
Ok((account, usk))
})
}
fn get_next_available_address(
&mut self,
account: AccountId,

View File

@ -179,6 +179,60 @@ pub fn get_address<P: consensus::Parameters>(
})
}
pub(crate) fn get_max_account_id<P>(
wdb: &WalletDb<P>,
) -> Result<Option<AccountId>, SqliteClientError> {
// This returns the most recently generated address.
Ok(wdb
.conn
.query_row("SELECT MAX(account) FROM accounts", NO_PARAMS, |row| {
row.get::<_, u32>(0).map(AccountId::from)
})
.optional()?)
}
pub(crate) fn add_account<P: consensus::Parameters>(
wdb: &WalletDb<P>,
account: AccountId,
key: &UnifiedFullViewingKey,
) -> Result<(), SqliteClientError> {
add_account_internal(&wdb.conn, &wdb.params, "accounts", account, key)
}
pub(crate) fn add_account_internal<P: consensus::Parameters, E: From<rusqlite::Error>>(
conn: &rusqlite::Connection,
network: &P,
accounts_table: &'static str,
account: AccountId,
key: &UnifiedFullViewingKey,
) -> Result<(), E> {
let ufvk_str: String = key.encode(network);
conn.execute_named(
&format!(
"INSERT INTO {} (account, ufvk) VALUES (:account, :ufvk)",
accounts_table
),
&[(":account", &<u32>::from(account)), (":ufvk", &ufvk_str)],
)?;
// Always derive the default Unified Address for the account.
let (address, mut idx) = key.default_address();
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(
"INSERT INTO addresses (account, diversifier_index_be, address)
VALUES (:account, :diversifier_index_be, :address)",
&[
(":account", &<u32>::from(account)),
(":diversifier_index_be", &&idx.0[..]),
(":address", &address_str),
],
)?;
Ok(())
}
pub(crate) fn get_current_address<P: consensus::Parameters>(
wdb: &WalletDb<P>,
account: AccountId,

View File

@ -1,5 +1,5 @@
//! Functions for initializing the various databases.
use rusqlite::{self, params, types::ToSql, Connection, NO_PARAMS};
use rusqlite::{self, params, types::ToSql, NO_PARAMS};
use schemer::{migration, Migration, Migrator, MigratorError};
use schemer_rusqlite::{RusqliteAdapter, RusqliteMigration};
use secrecy::{ExposeSecret, SecretVec};
@ -19,7 +19,11 @@ use zcash_client_backend::{
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
};
use crate::{error::SqliteClientError, wallet::PoolType, WalletDb};
use crate::{
error::SqliteClientError,
wallet::{self, PoolType},
WalletDb,
};
#[cfg(feature = "transparent-inputs")]
use {
@ -516,12 +520,17 @@ fn init_wallet_db_internal<P: consensus::Parameters + 'static>(
Ok(())
}
/// Initialises the data database with the given [`UnifiedFullViewingKey`]s.
/// Initialises the data database with the given set of account [`UnifiedFullViewingKey`]s.
///
/// **WARNING** This method should be used with care, and should ordinarily be unnecessary.
/// Prefer to use [`zcash_client_backend::data_api::WalletWrite::create_account`] instead.
///
/// The [`UnifiedFullViewingKey`]s are stored internally and used by other APIs such as
/// [`get_address`], [`scan_cached_blocks`], and [`create_spend_to_address`]. `extfvks` **MUST**
/// be arranged in account-order; that is, the [`UnifiedFullViewingKey`] for ZIP 32
/// account `i` **MUST** be at `extfvks[i]`.
/// [`get_address`], [`scan_cached_blocks`], and [`create_spend_to_address`]. Account identifiers
/// in `keys` **MUST** form a consecutive sequence beginning at account 0, and the
/// [`UnifiedFullViewingKey`] corresponding to a given account identifier **MUST** be derived from
/// the wallet's mnemonic seed at the BIP-44 `account` path level as described by
/// [ZIP 316](https://zips.z.cash/zip-0316)
///
/// # Examples
///
@ -578,53 +587,13 @@ pub fn init_accounts_table<P: consensus::Parameters>(
// Insert accounts atomically
wdb.conn.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
for (account, key) in keys.iter() {
add_account_internal::<P, SqliteClientError>(
&wdb.params,
&wdb.conn,
"accounts",
*account,
key,
)?;
wallet::add_account(wdb, *account, key)?;
}
wdb.conn.execute("COMMIT", NO_PARAMS)?;
Ok(())
}
fn add_account_internal<P: consensus::Parameters, E: From<rusqlite::Error>>(
network: &P,
conn: &Connection,
accounts_table: &'static str,
account: AccountId,
key: &UnifiedFullViewingKey,
) -> Result<(), E> {
let ufvk_str: String = key.encode(network);
conn.execute_named(
&format!(
"INSERT INTO {} (account, ufvk) VALUES (:account, :ufvk)",
accounts_table
),
&[(":account", &<u32>::from(account)), (":ufvk", &ufvk_str)],
)?;
// Always derive the default Unified Address for the account.
let (address, mut idx) = key.default_address();
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(
"INSERT INTO addresses (account, diversifier_index_be, address)
VALUES (:account, :diversifier_index_be, :address)",
&[
(":account", &<u32>::from(account)),
(":diversifier_index_be", &&idx.0[..]),
(":address", &address_str),
],
)?;
Ok(())
}
/// Initialises the data database with the given block.
///
/// This enables a newly-created database to be immediately-usable, without needing to

View File

@ -7,7 +7,8 @@ use uuid::Uuid;
use zcash_client_backend::{address::RecipientAddress, keys::UnifiedFullViewingKey};
use zcash_primitives::{consensus, zip32::AccountId};
use super::super::{add_account_internal, WalletMigrationError};
use super::super::WalletMigrationError;
use crate::wallet::add_account_internal;
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::legacy::keys::IncomingViewingKey;
@ -153,8 +154,8 @@ impl<P: consensus::Parameters> RusqliteMigration for AddressesTableMigration<P>
}
add_account_internal::<P, WalletMigrationError>(
&self.params,
transaction,
&self.params,
"accounts_new",
account,
&ufvk,