Merge pull request #661 from nuttycom/wallet/spend_with_usk

Use unified spending keys for spends & shielding.
This commit is contained in:
Kris Nuttycombe 2022-10-12 12:15:34 -06:00 committed by GitHub
commit f7a3b9bda3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 314 additions and 337 deletions

View File

@ -35,6 +35,7 @@ and this library adheres to Rust's notion of
- `Recipient` - `Recipient`
- `SentTransactionOutput` - `SentTransactionOutput`
- `WalletRead::get_unified_full_viewing_keys` - `WalletRead::get_unified_full_viewing_keys`
- `WalletRead::get_account_for_ufvk`
- `WalletRead::get_current_address` - `WalletRead::get_current_address`
- `WalletRead::get_all_nullifiers` - `WalletRead::get_all_nullifiers`
- `WalletWrite::create_account` - `WalletWrite::create_account`
@ -132,6 +133,8 @@ and this library adheres to Rust's notion of
- `decode_extended_spending_key` - `decode_extended_spending_key`
- `decode_extended_full_viewing_key` - `decode_extended_full_viewing_key`
- `decode_payment_address` - `decode_payment_address`
- `data_api::wallet::create_spend_to_address` has been modified to use a unified
spending key rather than a Sapling extended spending key.
### Removed ### Removed
- `zcash_client_backend::data_api`: - `zcash_client_backend::data_api`:

View File

@ -128,6 +128,13 @@ pub trait WalletRead {
&self, &self,
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error>; ) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error>;
/// Returns the account id corresponding to a given [`UnifiedFullViewingKey`],
/// if any.
fn get_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<AccountId>, Self::Error>;
/// Checks whether the specified extended full viewing key is /// Checks whether the specified extended full viewing key is
/// associated with the account. /// associated with the account.
fn is_valid_account_extfvk( fn is_valid_account_extfvk(
@ -484,6 +491,13 @@ pub mod testing {
Ok(HashMap::new()) Ok(HashMap::new())
} }
fn get_account_for_ufvk(
&self,
_ufvk: &UnifiedFullViewingKey,
) -> Result<Option<AccountId>, Self::Error> {
Ok(None)
}
fn is_valid_account_extfvk( fn is_valid_account_extfvk(
&self, &self,
_account: AccountId, _account: AccountId,

View File

@ -24,6 +24,9 @@ pub enum ChainInvalid {
#[derive(Debug)] #[derive(Debug)]
pub enum Error<NoteId> { pub enum Error<NoteId> {
/// No account could be found corresponding to a provided spending key.
KeyNotRecognized,
/// No account with the given identifier was found in the wallet. /// No account with the given identifier was found in the wallet.
AccountNotFound(AccountId), AccountNotFound(AccountId),
@ -90,6 +93,9 @@ impl ChainInvalid {
impl<N: fmt::Display> fmt::Display for Error<N> { impl<N: fmt::Display> fmt::Display for Error<N> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self { match &self {
Error::KeyNotRecognized => {
write!(f, "Wallet does not contain an account corresponding to the provided spending key")
}
Error::AccountNotFound(account) => { Error::AccountNotFound(account) => {
write!(f, "Wallet does not contain account {}", u32::from(*account)) write!(f, "Wallet does not contain account {}", u32::from(*account))
} }

View File

@ -8,17 +8,10 @@ use zcash_primitives::{
components::{amount::DEFAULT_FEE, Amount}, components::{amount::DEFAULT_FEE, Amount},
Transaction, Transaction,
}, },
zip32::{AccountId, ExtendedFullViewingKey, ExtendedSpendingKey},
}; };
#[cfg(feature = "transparent-inputs")] #[cfg(feature = "transparent-inputs")]
use { use zcash_primitives::{legacy::keys::IncomingViewingKey, sapling::keys::OutgoingViewingKey};
zcash_address::unified::Typecode,
zcash_primitives::{
legacy::keys::{self as transparent, IncomingViewingKey},
sapling::keys::OutgoingViewingKey,
},
};
use crate::{ use crate::{
address::RecipientAddress, address::RecipientAddress,
@ -27,6 +20,7 @@ use crate::{
SentTransactionOutput, WalletWrite, SentTransactionOutput, WalletWrite,
}, },
decrypt_transaction, decrypt_transaction,
keys::UnifiedSpendingKey,
wallet::OvkPolicy, wallet::OvkPolicy,
zip321::{Payment, TransactionRequest}, zip321::{Payment, TransactionRequest},
}; };
@ -129,7 +123,7 @@ where
/// }; /// };
/// use zcash_proofs::prover::LocalTxProver; /// use zcash_proofs::prover::LocalTxProver;
/// use zcash_client_backend::{ /// use zcash_client_backend::{
/// keys::sapling, /// keys::UnifiedSpendingKey,
/// data_api::{wallet::create_spend_to_address, error::Error, testing}, /// data_api::{wallet::create_spend_to_address, error::Error, testing},
/// wallet::OvkPolicy, /// wallet::OvkPolicy,
/// }; /// };
@ -148,8 +142,8 @@ where
/// }; /// };
/// ///
/// let account = AccountId::from(0); /// let account = AccountId::from(0);
/// let extsk = sapling::spending_key(&[0; 32][..], COIN_TYPE, account); /// let usk = UnifiedSpendingKey::from_seed(&Network::TestNetwork, &[0; 32][..], account).unwrap();
/// let to = extsk.default_address().1.into(); /// let to = usk.to_unified_full_viewing_key().default_address().0.into();
/// ///
/// let mut db_read = testing::MockWalletDb { /// let mut db_read = testing::MockWalletDb {
/// network: Network::TestNetwork /// network: Network::TestNetwork
@ -159,8 +153,7 @@ where
/// &mut db_read, /// &mut db_read,
/// &Network::TestNetwork, /// &Network::TestNetwork,
/// tx_prover, /// tx_prover,
/// account, /// &usk,
/// &extsk,
/// &to, /// &to,
/// Amount::from_u64(1).unwrap(), /// Amount::from_u64(1).unwrap(),
/// None, /// None,
@ -176,8 +169,7 @@ pub fn create_spend_to_address<E, N, P, D, R>(
wallet_db: &mut D, wallet_db: &mut D,
params: &P, params: &P,
prover: impl TxProver, prover: impl TxProver,
account: AccountId, usk: &UnifiedSpendingKey,
extsk: &ExtendedSpendingKey,
to: &RecipientAddress, to: &RecipientAddress,
amount: Amount, amount: Amount,
memo: Option<MemoBytes>, memo: Option<MemoBytes>,
@ -206,8 +198,7 @@ where
wallet_db, wallet_db,
params, params,
prover, prover,
extsk, usk,
account,
&req, &req,
ovk_policy, ovk_policy,
min_confirmations, min_confirmations,
@ -248,7 +239,7 @@ where
/// * `wallet_db`: A read/write reference to the wallet database /// * `wallet_db`: A read/write reference to the wallet database
/// * `params`: Consensus parameters /// * `params`: Consensus parameters
/// * `prover`: The TxProver to use in constructing the shielded transaction. /// * `prover`: The TxProver to use in constructing the shielded transaction.
/// * `extsk`: The extended spending key that controls the funds that will be spent /// * `usk`: The unified spending key that controls the funds that will be spent
/// in the resulting transaction. /// in the resulting transaction.
/// * `account`: The ZIP32 account identifier associated with the extended spending /// * `account`: The ZIP32 account identifier associated with the extended spending
/// key that controls the funds to be used in creating this transaction. This /// key that controls the funds to be used in creating this transaction. This
@ -265,8 +256,7 @@ pub fn spend<E, N, P, D, R>(
wallet_db: &mut D, wallet_db: &mut D,
params: &P, params: &P,
prover: impl TxProver, prover: impl TxProver,
extsk: &ExtendedSpendingKey, usk: &UnifiedSpendingKey,
account: AccountId,
request: &TransactionRequest, request: &TransactionRequest,
ovk_policy: OvkPolicy, ovk_policy: OvkPolicy,
min_confirmations: u32, min_confirmations: u32,
@ -277,12 +267,11 @@ where
R: Copy + Debug, R: Copy + Debug,
D: WalletWrite<Error = E, TxRef = R>, D: WalletWrite<Error = E, TxRef = R>,
{ {
// Check that the ExtendedSpendingKey we have been given corresponds to the let account = wallet_db
// ExtendedFullViewingKey for the account we are spending from. .get_account_for_ufvk(&usk.to_unified_full_viewing_key())?
let extfvk = ExtendedFullViewingKey::from(extsk); .ok_or(Error::KeyNotRecognized)?;
if !wallet_db.is_valid_account_extfvk(account, &extfvk)? {
return Err(E::from(Error::InvalidExtSk(account))); let extfvk = usk.sapling().to_extended_full_viewing_key();
}
// Apply the outgoing viewing key policy. // Apply the outgoing viewing key policy.
let ovk = match ovk_policy { let ovk = match ovk_policy {
@ -335,7 +324,12 @@ where
let merkle_path = selected.witness.path().expect("the tree is not empty"); let merkle_path = selected.witness.path().expect("the tree is not empty");
builder builder
.add_sapling_spend(extsk.clone(), selected.diversifier, note, merkle_path) .add_sapling_spend(
usk.sapling().clone(),
selected.diversifier,
note,
merkle_path,
)
.map_err(Error::Builder)?; .map_err(Error::Builder)?;
} }
@ -452,8 +446,7 @@ pub fn shield_transparent_funds<E, N, P, D, R, U>(
wallet_db: &mut D, wallet_db: &mut D,
params: &P, params: &P,
prover: impl TxProver, prover: impl TxProver,
sk: &transparent::AccountPrivKey, usk: &UnifiedSpendingKey,
account: AccountId,
memo: &MemoBytes, memo: &MemoBytes,
min_confirmations: u32, min_confirmations: u32,
) -> Result<D::TxRef, E> ) -> Result<D::TxRef, E>
@ -463,28 +456,20 @@ where
R: Copy + Debug, R: Copy + Debug,
D: WalletWrite<Error = E, TxRef = R> + WalletWriteTransparent<UtxoRef = U>, D: WalletWrite<Error = E, TxRef = R> + WalletWriteTransparent<UtxoRef = U>,
{ {
// Obtain the UFVK for the specified account & use its internal change address let account = wallet_db
// as the destination for shielded funds. .get_account_for_ufvk(&usk.to_unified_full_viewing_key())?
let shielding_address = wallet_db .ok_or(Error::KeyNotRecognized)?;
.get_unified_full_viewing_keys()
.and_then(|ufvks| {
ufvks
.get(&account)
.ok_or_else(|| E::from(Error::AccountNotFound(account)))
.and_then(|ufvk| {
// TODO: select the most preferred shielded receiver once we have the ability to
// spend Orchard funds.
ufvk.sapling()
.map(|dfvk| dfvk.change_address().1)
.ok_or_else(|| E::from(Error::KeyNotFound(account, Typecode::Sapling)))
})
})?;
let shielding_address = usk
.sapling()
.to_diversifiable_full_viewing_key()
.change_address()
.1;
let (latest_scanned_height, latest_anchor) = wallet_db let (latest_scanned_height, latest_anchor) = wallet_db
.get_target_and_anchor_heights(min_confirmations) .get_target_and_anchor_heights(min_confirmations)
.and_then(|x| x.ok_or_else(|| Error::ScanRequired.into()))?; .and_then(|x| x.ok_or_else(|| Error::ScanRequired.into()))?;
let account_pubkey = sk.to_account_pubkey(); let account_pubkey = usk.transparent().to_account_pubkey();
let ovk = OutgoingViewingKey(account_pubkey.internal_ovk().as_bytes()); let ovk = OutgoingViewingKey(account_pubkey.internal_ovk().as_bytes());
// derive the t-address for the extpubkey at the minimum valid child index // derive the t-address for the extpubkey at the minimum valid child index
@ -510,7 +495,10 @@ where
let mut builder = Builder::new_with_fee(params.clone(), latest_scanned_height, fee); let mut builder = Builder::new_with_fee(params.clone(), latest_scanned_height, fee);
let secret_key = sk.derive_external_secret_key(child_index).unwrap(); let secret_key = usk
.transparent()
.derive_external_secret_key(child_index)
.unwrap();
for utxo in &utxos { for utxo in &utxos {
builder builder
.add_transparent_input(secret_key, utxo.outpoint.clone(), utxo.txout.clone()) .add_transparent_input(secret_key, utxo.outpoint.clone(), utxo.txout.clone())

View File

@ -163,6 +163,13 @@ impl<P: consensus::Parameters> WalletRead for WalletDb<P> {
wallet::get_unified_full_viewing_keys(self) wallet::get_unified_full_viewing_keys(self)
} }
fn get_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<AccountId>, Self::Error> {
wallet::get_account_for_ufvk(self, ufvk)
}
fn get_current_address( fn get_current_address(
&self, &self,
account: AccountId, account: AccountId,
@ -289,6 +296,13 @@ 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_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<AccountId>, Self::Error> {
self.wallet_db.get_account_for_ufvk(ufvk)
}
fn get_current_address( fn get_current_address(
&self, &self,
account: AccountId, account: AccountId,

View File

@ -342,6 +342,25 @@ pub(crate) fn get_unified_full_viewing_keys<P: consensus::Parameters>(
Ok(res) Ok(res)
} }
/// Returns the account id corresponding to a given [`UnifiedFullViewingKey`],
/// if any.
pub(crate) fn get_account_for_ufvk<P: consensus::Parameters>(
wdb: &WalletDb<P>,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<AccountId>, SqliteClientError> {
wdb.conn
.query_row(
"SELECT account FROM accounts WHERE ufvk = ?",
[&ufvk.encode(&wdb.params)],
|row| {
let acct: u32 = row.get(0)?;
Ok(AccountId::from(acct))
},
)
.optional()
.map_err(SqliteClientError::from)
}
/// Checks whether the specified [`ExtendedFullViewingKey`] is valid and corresponds to the /// Checks whether the specified [`ExtendedFullViewingKey`] is valid and corresponds to the
/// specified account. /// specified account.
/// ///

View File

@ -157,28 +157,25 @@ pub fn select_spendable_sapling_notes<P>(
mod tests { mod tests {
use rusqlite::Connection; use rusqlite::Connection;
use secrecy::Secret; use secrecy::Secret;
use std::collections::HashMap;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use zcash_proofs::prover::LocalTxProver; use zcash_proofs::prover::LocalTxProver;
use zcash_primitives::{ use zcash_primitives::{
block::BlockHash, block::BlockHash,
consensus::{BlockHeight, BranchId, Parameters}, consensus::{BlockHeight, BranchId},
legacy::TransparentAddress, legacy::TransparentAddress,
sapling::{note_encryption::try_sapling_output_recovery, prover::TxProver}, sapling::{note_encryption::try_sapling_output_recovery, prover::TxProver},
transaction::{components::Amount, Transaction}, transaction::{components::Amount, Transaction},
zip32::sapling::{ zip32::sapling::{ExtendedFullViewingKey, ExtendedSpendingKey},
DiversifiableFullViewingKey, ExtendedFullViewingKey, ExtendedSpendingKey,
},
}; };
#[cfg(feature = "transparent-inputs")]
use zcash_primitives::legacy::keys as transparent;
use zcash_client_backend::{ use zcash_client_backend::{
data_api::{chain::scan_cached_blocks, wallet::create_spend_to_address, WalletRead}, data_api::{
keys::{sapling, UnifiedFullViewingKey}, self, chain::scan_cached_blocks, wallet::create_spend_to_address, WalletRead,
WalletWrite,
},
keys::UnifiedSpendingKey,
wallet::OvkPolicy, wallet::OvkPolicy,
}; };
@ -187,7 +184,7 @@ mod tests {
tests::{self, fake_compact_block, insert_into_cache, network, sapling_activation_height}, tests::{self, fake_compact_block, insert_into_cache, network, sapling_activation_height},
wallet::{ wallet::{
get_balance, get_balance_at, get_balance, get_balance_at,
init::{init_accounts_table, init_blocks_table, init_wallet_db}, init::{init_blocks_table, init_wallet_db},
}, },
AccountId, BlockDb, DataConnStmtCache, WalletDb, AccountId, BlockDb, DataConnStmtCache, WalletDb,
}; };
@ -202,132 +199,80 @@ mod tests {
} }
#[test] #[test]
fn create_to_address_fails_on_incorrect_extsk() { fn create_to_address_fails_on_incorrect_usk() {
let data_file = NamedTempFile::new().unwrap(); let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
let acct0 = AccountId::from(0); // Add an account to the wallet
let mut ops = db_data.get_update_ops().unwrap();
let seed = Secret::new([0u8; 32].to_vec());
let (_, usk) = ops.create_account(&seed).unwrap();
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
let to = dfvk.default_address().1.into();
// Create a USK that doesn't exist in the wallet
let acct1 = AccountId::from(1); let acct1 = AccountId::from(1);
let usk1 = UnifiedSpendingKey::from_seed(&network(), &[1u8; 32], acct1).unwrap();
// Add two accounts to the wallet // Attempting to spend with a USK that is not in the wallet results in an error
let extsk0 = sapling::spending_key(&[0u8; 32], network().coin_type(), acct0);
let extsk1 = sapling::spending_key(&[1u8; 32], network().coin_type(), acct1);
let dfvk0 = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk0));
let dfvk1 = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk1));
#[cfg(feature = "transparent-inputs")]
let ufvks = {
let tsk0 =
transparent::AccountPrivKey::from_seed(&network(), &[0u8; 32], acct0).unwrap();
let tsk1 =
transparent::AccountPrivKey::from_seed(&network(), &[1u8; 32], acct1).unwrap();
HashMap::from([
(
acct0,
UnifiedFullViewingKey::new(Some(tsk0.to_account_pubkey()), Some(dfvk0), None)
.unwrap(),
),
(
acct1,
UnifiedFullViewingKey::new(Some(tsk1.to_account_pubkey()), Some(dfvk1), None)
.unwrap(),
),
])
};
#[cfg(not(feature = "transparent-inputs"))]
let ufvks = HashMap::from([
(
acct0,
UnifiedFullViewingKey::new(Some(dfvk0), None).unwrap(),
),
(
acct1,
UnifiedFullViewingKey::new(Some(dfvk1), None).unwrap(),
),
]);
init_accounts_table(&db_data, &ufvks).unwrap();
let to = extsk0.default_address().1.into();
// Invalid extsk for the given account should cause an error
let mut db_write = db_data.get_update_ops().unwrap(); let mut db_write = db_data.get_update_ops().unwrap();
match create_spend_to_address( assert!(matches!(
&mut db_write, create_spend_to_address(
&tests::network(), &mut db_write,
test_prover(), &tests::network(),
AccountId::from(0), test_prover(),
&extsk1, &usk1,
&to, &to,
Amount::from_u64(1).unwrap(), Amount::from_u64(1).unwrap(),
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
10, 10,
) { ),
Ok(_) => panic!("Should have failed"), Err(crate::SqliteClientError::BackendError(
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"), data_api::error::Error::KeyNotRecognized
} ))
));
match create_spend_to_address(
&mut db_write,
&tests::network(),
test_prover(),
AccountId::from(1),
&extsk0,
&to,
Amount::from_u64(1).unwrap(),
None,
OvkPolicy::Sender,
10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 1"),
}
} }
#[test] #[test]
fn create_to_address_fails_with_no_blocks() { fn create_to_address_fails_with_no_blocks() {
let data_file = NamedTempFile::new().unwrap(); let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); init_wallet_db(&mut db_data, None).unwrap();
// Add an account to the wallet // Add an account to the wallet
let account_id = AccountId::from(0); let mut ops = db_data.get_update_ops().unwrap();
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id); let seed = Secret::new([0u8; 32].to_vec());
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); let (_, usk) = ops.create_account(&seed).unwrap();
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
#[cfg(feature = "transparent-inputs")] let to = dfvk.default_address().1.into();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
let to = extsk.default_address().1.into();
// We cannot do anything if we aren't synchronised // We cannot do anything if we aren't synchronised
let mut db_write = db_data.get_update_ops().unwrap(); let mut db_write = db_data.get_update_ops().unwrap();
match create_spend_to_address( assert!(matches!(
&mut db_write, create_spend_to_address(
&tests::network(), &mut db_write,
test_prover(), &tests::network(),
AccountId::from(0), test_prover(),
&extsk, &usk,
&to, &to,
Amount::from_u64(1).unwrap(), Amount::from_u64(1).unwrap(),
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
10, 10,
) { ),
Ok(_) => panic!("Should have failed"), Err(crate::SqliteClientError::BackendError(
Err(e) => assert_eq!(e.to_string(), "Must scan blocks first"), data_api::error::Error::ScanRequired
} ))
));
} }
#[test] #[test]
fn create_to_address_fails_on_insufficient_balance() { fn create_to_address_fails_on_insufficient_balance() {
let data_file = NamedTempFile::new().unwrap(); let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); init_wallet_db(&mut db_data, None).unwrap();
init_blocks_table( init_blocks_table(
&db_data, &db_data,
BlockHeight::from(1u32), BlockHeight::from(1u32),
@ -338,16 +283,11 @@ mod tests {
.unwrap(); .unwrap();
// Add an account to the wallet // Add an account to the wallet
let account_id = AccountId::from(0); let mut ops = db_data.get_update_ops().unwrap();
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id); let seed = Secret::new([0u8; 32].to_vec());
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); let (_, usk) = ops.create_account(&seed).unwrap();
#[cfg(feature = "transparent-inputs")] let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk), None).unwrap(); let to = dfvk.default_address().1.into();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
let to = extsk.default_address().1.into();
// Account balance should be zero // Account balance should be zero
assert_eq!( assert_eq!(
@ -357,24 +297,26 @@ mod tests {
// We cannot spend anything // We cannot spend anything
let mut db_write = db_data.get_update_ops().unwrap(); let mut db_write = db_data.get_update_ops().unwrap();
match create_spend_to_address( assert!(matches!(
&mut db_write, create_spend_to_address(
&tests::network(), &mut db_write,
test_prover(), &tests::network(),
AccountId::from(0), test_prover(),
&extsk, &usk,
&to, &to,
Amount::from_u64(1).unwrap(), Amount::from_u64(1).unwrap(),
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
10, 10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient balance (have 0, need 1001 including fee)"
), ),
} Err(crate::SqliteClientError::BackendError(
data_api::error::Error::InsufficientBalance(
available,
required
)
))
if available == Amount::zero() && required == Amount::from_u64(1001).unwrap()
));
} }
#[test] #[test]
@ -385,18 +327,13 @@ mod tests {
let data_file = NamedTempFile::new().unwrap(); let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); init_wallet_db(&mut db_data, None).unwrap();
// Add an account to the wallet // Add an account to the wallet
let account_id = AccountId::from(0); let mut ops = db_data.get_update_ops().unwrap();
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id); let seed = Secret::new([0u8; 32].to_vec());
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); let (_, usk) = ops.create_account(&seed).unwrap();
#[cfg(feature = "transparent-inputs")] let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
// Add funds to the wallet in a single note // Add funds to the wallet in a single note
let value = Amount::from_u64(50000).unwrap(); let value = Amount::from_u64(50000).unwrap();
@ -437,24 +374,27 @@ mod tests {
// Spend fails because there are insufficient verified notes // Spend fails because there are insufficient verified notes
let extsk2 = ExtendedSpendingKey::master(&[]); let extsk2 = ExtendedSpendingKey::master(&[]);
let to = extsk2.default_address().1.into(); let to = extsk2.default_address().1.into();
match create_spend_to_address( assert!(matches!(
&mut db_write, create_spend_to_address(
&tests::network(), &mut db_write,
test_prover(), &tests::network(),
AccountId::from(0), test_prover(),
&extsk, &usk,
&to, &to,
Amount::from_u64(70000).unwrap(), Amount::from_u64(70000).unwrap(),
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
10, 10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient balance (have 50000, need 71000 including fee)"
), ),
} Err(crate::SqliteClientError::BackendError(
data_api::error::Error::InsufficientBalance(
available,
required
)
))
if available == Amount::from_u64(50000).unwrap()
&& required == Amount::from_u64(71000).unwrap()
));
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second // Mine blocks SAPLING_ACTIVATION_HEIGHT + 2 to 9 until just before the second
// note is verified // note is verified
@ -466,24 +406,27 @@ mod tests {
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Second spend still fails // Second spend still fails
match create_spend_to_address( assert!(matches!(
&mut db_write, create_spend_to_address(
&tests::network(), &mut db_write,
test_prover(), &tests::network(),
AccountId::from(0), test_prover(),
&extsk, &usk,
&to, &to,
Amount::from_u64(70000).unwrap(), Amount::from_u64(70000).unwrap(),
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
10, 10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient balance (have 50000, need 71000 including fee)"
), ),
} Err(crate::SqliteClientError::BackendError(
data_api::error::Error::InsufficientBalance(
available,
required
)
))
if available == Amount::from_u64(50000).unwrap()
&& required == Amount::from_u64(71000).unwrap()
));
// Mine block 11 so that the second note becomes verified // Mine block 11 so that the second note becomes verified
let (cb, _) = fake_compact_block(sapling_activation_height() + 10, cb.hash(), &dfvk, value); let (cb, _) = fake_compact_block(sapling_activation_height() + 10, cb.hash(), &dfvk, value);
@ -491,19 +434,20 @@ mod tests {
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Second spend should now succeed // Second spend should now succeed
create_spend_to_address( assert!(matches!(
&mut db_write, create_spend_to_address(
&tests::network(), &mut db_write,
test_prover(), &tests::network(),
AccountId::from(0), test_prover(),
&extsk, &usk,
&to, &to,
Amount::from_u64(70000).unwrap(), Amount::from_u64(70000).unwrap(),
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
10, 10,
) ),
.unwrap(); Ok(_)
));
} }
#[test] #[test]
@ -517,15 +461,10 @@ mod tests {
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap();
// Add an account to the wallet // Add an account to the wallet
let account_id = AccountId::from(0); let mut ops = db_data.get_update_ops().unwrap();
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id); let seed = Secret::new([0u8; 32].to_vec());
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); let (_, usk) = ops.create_account(&seed).unwrap();
#[cfg(feature = "transparent-inputs")] let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
// Add funds to the wallet in a single note // Add funds to the wallet in a single note
let value = Amount::from_u64(50000).unwrap(); let value = Amount::from_u64(50000).unwrap();
@ -543,39 +482,42 @@ mod tests {
// Send some of the funds to another address // Send some of the funds to another address
let extsk2 = ExtendedSpendingKey::master(&[]); let extsk2 = ExtendedSpendingKey::master(&[]);
let to = extsk2.default_address().1.into(); let to = extsk2.default_address().1.into();
create_spend_to_address( assert!(matches!(
&mut db_write, create_spend_to_address(
&tests::network(), &mut db_write,
test_prover(), &tests::network(),
AccountId::from(0), test_prover(),
&extsk, &usk,
&to, &to,
Amount::from_u64(15000).unwrap(), Amount::from_u64(15000).unwrap(),
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
10, 10,
) ),
.unwrap(); Ok(_)
));
// A second spend fails because there are no usable notes // A second spend fails because there are no usable notes
match create_spend_to_address( assert!(matches!(
&mut db_write, create_spend_to_address(
&tests::network(), &mut db_write,
test_prover(), &tests::network(),
AccountId::from(0), test_prover(),
&extsk, &usk,
&to, &to,
Amount::from_u64(2000).unwrap(), Amount::from_u64(2000).unwrap(),
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
10, 10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient balance (have 0, need 3000 including fee)"
), ),
} Err(crate::SqliteClientError::BackendError(
data_api::error::Error::InsufficientBalance(
available,
required
)
))
if available == Amount::zero() && required == Amount::from_u64(3000).unwrap()
));
// Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 21 (that don't send us funds) // Mine blocks SAPLING_ACTIVATION_HEIGHT + 1 to 21 (that don't send us funds)
// until just before the first transaction expires // until just before the first transaction expires
@ -591,24 +533,26 @@ mod tests {
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap(); scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Second spend still fails // Second spend still fails
match create_spend_to_address( assert!(matches!(
&mut db_write, create_spend_to_address(
&tests::network(), &mut db_write,
test_prover(), &tests::network(),
AccountId::from(0), test_prover(),
&extsk, &usk,
&to, &to,
Amount::from_u64(2000).unwrap(), Amount::from_u64(2000).unwrap(),
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
10, 10,
) {
Ok(_) => panic!("Should have failed"),
Err(e) => assert_eq!(
e.to_string(),
"Insufficient balance (have 0, need 3000 including fee)"
), ),
} Err(crate::SqliteClientError::BackendError(
data_api::error::Error::InsufficientBalance(
available,
required
)
))
if available == Amount::zero() && required == Amount::from_u64(3000).unwrap()
));
// Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires // Mine block SAPLING_ACTIVATION_HEIGHT + 22 so that the first transaction expires
let (cb, _) = fake_compact_block( let (cb, _) = fake_compact_block(
@ -625,8 +569,7 @@ mod tests {
&mut db_write, &mut db_write,
&tests::network(), &tests::network(),
test_prover(), test_prover(),
AccountId::from(0), &usk,
&extsk,
&to, &to,
Amount::from_u64(2000).unwrap(), Amount::from_u64(2000).unwrap(),
None, None,
@ -645,18 +588,13 @@ mod tests {
let data_file = NamedTempFile::new().unwrap(); let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap(); let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); init_wallet_db(&mut db_data, None).unwrap();
// Add an account to the wallet // Add an account to the wallet
let account_id = AccountId::from(0); let mut ops = db_data.get_update_ops().unwrap();
let extsk = sapling::spending_key(&[0u8; 32], network.coin_type(), account_id); let seed = Secret::new([0u8; 32].to_vec());
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); let (_, usk) = ops.create_account(&seed).unwrap();
#[cfg(feature = "transparent-inputs")] let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
// Add funds to the wallet in a single note // Add funds to the wallet in a single note
let value = Amount::from_u64(50000).unwrap(); let value = Amount::from_u64(50000).unwrap();
@ -680,8 +618,7 @@ mod tests {
db_write, db_write,
&tests::network(), &tests::network(),
test_prover(), test_prover(),
AccountId::from(0), &usk,
&extsk,
&to, &to,
Amount::from_u64(15000).unwrap(), Amount::from_u64(15000).unwrap(),
None, None,
@ -757,18 +694,13 @@ mod tests {
let data_file = NamedTempFile::new().unwrap(); let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap(); let mut db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&mut db_data, Some(Secret::new(vec![]))).unwrap(); init_wallet_db(&mut db_data, None).unwrap();
// Add an account to the wallet // Add an account to the wallet
let account_id = AccountId::from(0); let mut ops = db_data.get_update_ops().unwrap();
let extsk = sapling::spending_key(&[0u8; 32], network().coin_type(), account_id); let seed = Secret::new([0u8; 32].to_vec());
let dfvk = DiversifiableFullViewingKey::from(ExtendedFullViewingKey::from(&extsk)); let (_, usk) = ops.create_account(&seed).unwrap();
#[cfg(feature = "transparent-inputs")] let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
let ufvk = UnifiedFullViewingKey::new(None, Some(dfvk.clone()), None).unwrap();
#[cfg(not(feature = "transparent-inputs"))]
let ufvk = UnifiedFullViewingKey::new(Some(dfvk.clone()), None).unwrap();
let ufvks = HashMap::from([(account_id, ufvk)]);
init_accounts_table(&db_data, &ufvks).unwrap();
// Add funds to the wallet in a single note // Add funds to the wallet in a single note
let value = Amount::from_u64(51000).unwrap(); let value = Amount::from_u64(51000).unwrap();
@ -791,18 +723,19 @@ mod tests {
); );
let to = TransparentAddress::PublicKey([7; 20]).into(); let to = TransparentAddress::PublicKey([7; 20]).into();
create_spend_to_address( assert!(matches!(
&mut db_write, create_spend_to_address(
&tests::network(), &mut db_write,
test_prover(), &tests::network(),
AccountId::from(0), test_prover(),
&extsk, &usk,
&to, &to,
Amount::from_u64(50000).unwrap(), Amount::from_u64(50000).unwrap(),
None, None,
OvkPolicy::Sender, OvkPolicy::Sender,
10, 10,
) ),
.unwrap(); Ok(_)
));
} }
} }