Merge pull request #1530 from nuttycom/generalized_test_framework

Generalize the `zcash_client_sqlite` test framework and extract it to `zcash_client_backend`
This commit is contained in:
Jack Grigg 2024-09-10 16:45:47 +01:00 committed by GitHub
commit c97e9a192b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 3895 additions and 3454 deletions

16
Cargo.lock generated
View File

@ -67,6 +67,18 @@ version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "ambassador"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b27ba24e4d8a188489d5a03c7fabc167a60809a383cdb4d15feb37479cd2a48"
dependencies = [
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "amplify"
version = "4.6.0"
@ -5817,6 +5829,7 @@ dependencies = [
name = "zcash_client_backend"
version = "0.13.0"
dependencies = [
"ambassador",
"arti-client",
"assert_matches",
"async-trait",
@ -5841,10 +5854,12 @@ dependencies = [
"nom",
"nonempty",
"orchard",
"pasta_curves",
"percent-encoding",
"proptest",
"prost",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
"rayon",
"rust_decimal",
@ -5879,6 +5894,7 @@ dependencies = [
name = "zcash_client_sqlite"
version = "0.11.2"
dependencies = [
"ambassador",
"assert_matches",
"bip32",
"bls12_381",

View File

@ -133,6 +133,7 @@ lazy_static = "1"
static_assertions = "1"
# Tests and benchmarks
ambassador = "0.4"
assert_matches = "1.5"
criterion = "0.5"
proptest = "1"

View File

@ -654,7 +654,7 @@ mod parse {
)(input)
}
/// The primary parser for <name>=<value> query-string parameter pair.
/// The primary parser for `name=value` query-string parameter pairs.
pub fn zcashparam(input: &str) -> IResult<&str, IndexedParam> {
map_res(
separated_pair(indexed_name, char('='), recognize(qchars)),

View File

@ -7,6 +7,12 @@ description = "The cryptographic code in this crate has been reviewed for correc
[criteria.license-reviewed]
description = "The license of this crate has been reviewed for compatibility with its usage in this repository."
[[audits.ambassador]]
who = "Kris Nuttycombe <kris@nutty.land>"
criteria = "safe-to-deploy"
version = "0.4.1"
notes = "Crate uses no unsafe code and the macros introduced by this crate generate the expected trait implementations without introducing additional unexpected operations."
[[audits.anyhow]]
who = "Daira-Emma Hopwood <daira@jacaranda.org>"
criteria = "safe-to-deploy"

View File

@ -7,6 +7,11 @@ and this library adheres to Rust's notion of
## [Unreleased]
### Changed
- The `Account` trait now uses an associated type for its `AccountId`
type instead of a type parameter. This change allows for the simplification
of some type signatures.
## [0.13.0] - 2024-08-20
`zcash_client_backend` now supports TEX (transparent-source-only) addresses as specified

View File

@ -89,8 +89,13 @@ incrementalmerkletree.workspace = true
shardtree.workspace = true
# - Test dependencies
ambassador = { workspace = true, optional = true }
assert_matches = { workspace = true, optional = true }
pasta_curves = { workspace = true, optional = true }
proptest = { workspace = true, optional = true }
jubjub = { workspace = true, optional = true }
rand_chacha = { workspace = true, optional = true }
zcash_proofs = { workspace = true, optional = true }
# - ZIP 321
nom = "7"
@ -137,17 +142,21 @@ tonic-build = { workspace = true, features = ["prost"] }
which = "4"
[dev-dependencies]
ambassador.workspace = true
assert_matches.workspace = true
gumdrop = "0.8"
incrementalmerkletree = { workspace = true, features = ["test-dependencies"] }
jubjub.workspace = true
proptest.workspace = true
rand_core.workspace = true
rand.workspace = true
rand_chacha.workspace = true
shardtree = { workspace = true, features = ["test-dependencies"] }
zcash_proofs.workspace = true
tokio = { version = "1.21.0", features = ["rt-multi-thread"] }
zcash_address = { workspace = true, features = ["test-dependencies"] }
zcash_keys = { workspace = true, features = ["test-dependencies"] }
tokio = { version = "1.21.0", features = ["rt-multi-thread"] }
zcash_primitives = { workspace = true, features = ["test-dependencies"] }
zcash_proofs = { workspace = true, features = ["bundled-prover"] }
zcash_protocol = { workspace = true, features = ["local-consensus"] }
[features]
## Enables the `tonic` gRPC client bindings for connecting to a `lightwalletd` server.
@ -164,7 +173,7 @@ transparent-inputs = [
]
## Enables receiving and spending Orchard funds.
orchard = ["dep:orchard", "zcash_keys/orchard"]
orchard = ["dep:orchard", "dep:pasta_curves", "zcash_keys/orchard"]
## Exposes a wallet synchronization function that implements the necessary state machine.
sync = [
@ -195,11 +204,17 @@ tor = [
## Exposes APIs that are useful for testing, such as `proptest` strategies.
test-dependencies = [
"dep:ambassador",
"dep:assert_matches",
"dep:proptest",
"dep:jubjub",
"dep:rand",
"dep:rand_chacha",
"orchard?/test-dependencies",
"zcash_keys/test-dependencies",
"zcash_primitives/test-dependencies",
"zcash_proofs/bundled-prover",
"zcash_protocol/local-consensus",
"incrementalmerkletree/test-dependencies",
]
@ -214,6 +229,14 @@ unstable-serialization = ["dep:byteorder"]
## Exposes the [`data_api::scanning::spanning_tree`] module.
unstable-spanning-tree = []
## Exposes access to the lightwalletd server via TOR
tor-lightwalletd-tonic = [
"tor",
"lightwalletd-tonic",
"tonic?/tls",
"tonic?/tls-webpki-roots"
]
[lib]
bench = false

View File

@ -102,6 +102,9 @@ use {
zcash_primitives::legacy::TransparentAddress,
};
#[cfg(feature = "test-dependencies")]
use ambassador::delegatable_trait;
#[cfg(any(test, feature = "test-dependencies"))]
use zcash_primitives::consensus::NetworkUpgrade;
@ -110,6 +113,9 @@ pub mod error;
pub mod scanning;
pub mod wallet;
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing;
/// The height of subtree roots in the Sapling note commitment tree.
///
/// This conforms to the structure of subtree data returned by
@ -346,9 +352,11 @@ pub enum AccountSource {
}
/// A set of capabilities that a client account must provide.
pub trait Account<AccountId: Copy> {
pub trait Account {
type AccountId: Copy;
/// Returns the unique identifier for the account.
fn id(&self) -> AccountId;
fn id(&self) -> Self::AccountId;
/// Returns whether this account is derived or imported, and the derivation parameters
/// if applicable.
@ -377,7 +385,9 @@ pub trait Account<AccountId: Copy> {
}
#[cfg(any(test, feature = "test-dependencies"))]
impl<A: Copy> Account<A> for (A, UnifiedFullViewingKey) {
impl<A: Copy> Account for (A, UnifiedFullViewingKey) {
type AccountId = A;
fn id(&self) -> A {
self.0
}
@ -398,7 +408,9 @@ impl<A: Copy> Account<A> for (A, UnifiedFullViewingKey) {
}
#[cfg(any(test, feature = "test-dependencies"))]
impl<A: Copy> Account<A> for (A, UnifiedIncomingViewingKey) {
impl<A: Copy> Account for (A, UnifiedIncomingViewingKey) {
type AccountId = A;
fn id(&self) -> A {
self.0
}
@ -656,12 +668,23 @@ impl<NoteRef> SpendableNotes<NoteRef> {
self.sapling.as_ref()
}
/// Consumes this value and returns the Sapling notes contained within it.
pub fn take_sapling(self) -> Vec<ReceivedNote<NoteRef, sapling::Note>> {
self.sapling
}
/// Returns the set of spendable Orchard notes.
#[cfg(feature = "orchard")]
pub fn orchard(&self) -> &[ReceivedNote<NoteRef, orchard::note::Note>] {
self.orchard.as_ref()
}
/// Consumes this value and returns the Orchard notes contained within it.
#[cfg(feature = "orchard")]
pub fn take_orchard(self) -> Vec<ReceivedNote<NoteRef, orchard::note::Note>> {
self.orchard
}
/// Computes the total value of Sapling notes.
pub fn sapling_value(&self) -> Result<NonNegativeAmount, BalanceError> {
self.sapling
@ -715,6 +738,7 @@ impl<NoteRef> SpendableNotes<NoteRef> {
/// A trait representing the capability to query a data store for unspent transaction outputs
/// belonging to a wallet.
#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
pub trait InputSource {
/// The type of errors produced by a wallet backend.
type Error: Debug;
@ -792,6 +816,7 @@ pub trait InputSource {
/// This trait defines the read-only portion of the storage interface atop which
/// higher-level wallet operations are implemented. It serves to allow wallet functions to
/// be abstracted away from any particular data storage substrate.
#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
pub trait WalletRead {
/// The type of errors that may be generated when querying a wallet data store.
type Error: Debug;
@ -804,7 +829,7 @@ pub trait WalletRead {
type AccountId: Copy + Debug + Eq + Hash;
/// The concrete account type used by this wallet backend.
type Account: Account<Self::AccountId>;
type Account: Account<AccountId = Self::AccountId>;
/// Returns a vector with the IDs of all accounts known to this wallet.
fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error>;
@ -1141,6 +1166,29 @@ pub trait WalletRead {
/// transaction data requests, such as when it is necessary to fill in purely-transparent
/// transaction history by walking the chain backwards via transparent inputs.
fn transaction_data_requests(&self) -> Result<Vec<TransactionDataRequest>, Self::Error>;
/// Returns a vector of transaction summaries.
///
/// Currently test-only, as production use could return a very large number of results; either
/// pagination or a streaming design will be necessary to stabilize this feature for production
/// use.
#[cfg(any(test, feature = "test-dependencies"))]
fn get_tx_history(
&self,
) -> Result<Vec<testing::TransactionSummary<Self::AccountId>>, Self::Error> {
Ok(vec![])
}
/// Returns the note IDs for shielded notes sent by the wallet in a particular
/// transaction.
#[cfg(any(test, feature = "test-dependencies"))]
fn get_sent_note_ids(
&self,
_txid: &TxId,
_protocol: ShieldedProtocol,
) -> Result<Vec<NoteId>, Self::Error> {
Ok(vec![])
}
}
/// The relevance of a seed to a given wallet.
@ -1767,6 +1815,7 @@ impl AccountBirthday {
/// [zcash/librustzcash#1284]: https://github.com/zcash/librustzcash/issues/1284
/// [BIP 39]: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
/// [`bip0039`]: https://crates.io/crates/bip0039
#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
pub trait WalletWrite: WalletRead {
/// The type of identifiers used to look up transparent UTXOs.
type UtxoRef;
@ -1968,9 +2017,7 @@ pub trait WalletWrite: WalletRead {
}
/// This trait describes a capability for manipulating wallet note commitment trees.
///
/// At present, this only serves the Sapling protocol, but it will be modified to
/// also provide operations related to Orchard note commitment trees in the future.
#[cfg_attr(feature = "test-dependencies", delegatable_trait)]
pub trait WalletCommitmentTrees {
type Error: Debug;
@ -2037,465 +2084,3 @@ pub trait WalletCommitmentTrees {
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>>;
}
#[cfg(feature = "test-dependencies")]
pub mod testing {
use incrementalmerkletree::Address;
use secrecy::{ExposeSecret, SecretVec};
use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree};
use std::{collections::HashMap, convert::Infallible, num::NonZeroU32};
use zip32::fingerprint::SeedFingerprint;
use zcash_primitives::{
block::BlockHash,
consensus::{BlockHeight, Network},
memo::Memo,
transaction::{components::amount::NonNegativeAmount, Transaction, TxId},
};
use crate::{
address::UnifiedAddress,
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput},
ShieldedProtocol,
};
use super::{
chain::{ChainState, CommitmentTreeRoot},
scanning::ScanRange,
AccountBirthday, AccountPurpose, BlockMetadata, DecryptedTransaction, InputSource,
NullifierQuery, ScannedBlock, SeedRelevance, SentTransaction, SpendableNotes,
TransactionDataRequest, TransactionStatus, WalletCommitmentTrees, WalletRead,
WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
};
#[cfg(feature = "transparent-inputs")]
use {
crate::wallet::TransparentAddressMetadata, std::ops::Range,
zcash_primitives::legacy::TransparentAddress,
};
#[cfg(feature = "orchard")]
use super::ORCHARD_SHARD_HEIGHT;
pub struct MockWalletDb {
pub network: Network,
pub sapling_tree: ShardTree<
MemoryShardStore<sapling::Node, BlockHeight>,
{ SAPLING_SHARD_HEIGHT * 2 },
SAPLING_SHARD_HEIGHT,
>,
#[cfg(feature = "orchard")]
pub orchard_tree: ShardTree<
MemoryShardStore<orchard::tree::MerkleHashOrchard, BlockHeight>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
}
impl MockWalletDb {
pub fn new(network: Network) -> Self {
Self {
network,
sapling_tree: ShardTree::new(MemoryShardStore::empty(), 100),
#[cfg(feature = "orchard")]
orchard_tree: ShardTree::new(MemoryShardStore::empty(), 100),
}
}
}
impl InputSource for MockWalletDb {
type Error = ();
type NoteRef = u32;
type AccountId = u32;
fn get_spendable_note(
&self,
_txid: &TxId,
_protocol: ShieldedProtocol,
_index: u32,
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
Ok(None)
}
fn select_spendable_notes(
&self,
_account: Self::AccountId,
_target_value: NonNegativeAmount,
_sources: &[ShieldedProtocol],
_anchor_height: BlockHeight,
_exclude: &[Self::NoteRef],
) -> Result<SpendableNotes<Self::NoteRef>, Self::Error> {
Ok(SpendableNotes::empty())
}
}
impl WalletRead for MockWalletDb {
type Error = ();
type AccountId = u32;
type Account = (Self::AccountId, UnifiedFullViewingKey);
fn get_account_ids(&self) -> Result<Vec<Self::AccountId>, Self::Error> {
Ok(Vec::new())
}
fn get_account(
&self,
_account_id: Self::AccountId,
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}
fn get_derived_account(
&self,
_seed: &SeedFingerprint,
_account_id: zip32::AccountId,
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}
fn validate_seed(
&self,
_account_id: Self::AccountId,
_seed: &SecretVec<u8>,
) -> Result<bool, Self::Error> {
Ok(false)
}
fn seed_relevance_to_derived_accounts(
&self,
_seed: &SecretVec<u8>,
) -> Result<SeedRelevance<Self::AccountId>, Self::Error> {
Ok(SeedRelevance::NoAccounts)
}
fn get_account_for_ufvk(
&self,
_ufvk: &UnifiedFullViewingKey,
) -> Result<Option<Self::Account>, Self::Error> {
Ok(None)
}
fn get_current_address(
&self,
_account: Self::AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
Ok(None)
}
fn get_account_birthday(
&self,
_account: Self::AccountId,
) -> Result<BlockHeight, Self::Error> {
Err(())
}
fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_wallet_summary(
&self,
_min_confirmations: u32,
) -> Result<Option<WalletSummary<Self::AccountId>>, Self::Error> {
Ok(None)
}
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_block_hash(
&self,
_block_height: BlockHeight,
) -> Result<Option<BlockHash>, Self::Error> {
Ok(None)
}
fn block_metadata(
&self,
_height: BlockHeight,
) -> Result<Option<BlockMetadata>, Self::Error> {
Ok(None)
}
fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
Ok(None)
}
fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
Ok(None)
}
fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
Ok(None)
}
fn suggest_scan_ranges(&self) -> Result<Vec<ScanRange>, Self::Error> {
Ok(vec![])
}
fn get_target_and_anchor_heights(
&self,
_min_confirmations: NonZeroU32,
) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
Ok(None)
}
fn get_min_unspent_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_tx_height(&self, _txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
Ok(None)
}
fn get_unified_full_viewing_keys(
&self,
) -> Result<HashMap<Self::AccountId, UnifiedFullViewingKey>, Self::Error> {
Ok(HashMap::new())
}
fn get_memo(&self, _id_note: NoteId) -> Result<Option<Memo>, Self::Error> {
Ok(None)
}
fn get_transaction(&self, _txid: TxId) -> Result<Option<Transaction>, Self::Error> {
Ok(None)
}
fn get_sapling_nullifiers(
&self,
_query: NullifierQuery,
) -> Result<Vec<(Self::AccountId, sapling::Nullifier)>, Self::Error> {
Ok(Vec::new())
}
#[cfg(feature = "orchard")]
fn get_orchard_nullifiers(
&self,
_query: NullifierQuery,
) -> Result<Vec<(Self::AccountId, orchard::note::Nullifier)>, Self::Error> {
Ok(Vec::new())
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_receivers(
&self,
_account: Self::AccountId,
) -> Result<HashMap<TransparentAddress, Option<TransparentAddressMetadata>>, Self::Error>
{
Ok(HashMap::new())
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_balances(
&self,
_account: Self::AccountId,
_max_height: BlockHeight,
) -> Result<HashMap<TransparentAddress, NonNegativeAmount>, Self::Error> {
Ok(HashMap::new())
}
#[cfg(feature = "transparent-inputs")]
fn get_transparent_address_metadata(
&self,
_account: Self::AccountId,
_address: &TransparentAddress,
) -> Result<Option<TransparentAddressMetadata>, Self::Error> {
Ok(None)
}
#[cfg(feature = "transparent-inputs")]
fn get_known_ephemeral_addresses(
&self,
_account: Self::AccountId,
_index_range: Option<Range<u32>>,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, Self::Error> {
Ok(vec![])
}
#[cfg(feature = "transparent-inputs")]
fn find_account_for_ephemeral_address(
&self,
_address: &TransparentAddress,
) -> Result<Option<Self::AccountId>, Self::Error> {
Ok(None)
}
fn transaction_data_requests(&self) -> Result<Vec<TransactionDataRequest>, Self::Error> {
Ok(vec![])
}
}
impl WalletWrite for MockWalletDb {
type UtxoRef = u32;
fn create_account(
&mut self,
seed: &SecretVec<u8>,
_birthday: &AccountBirthday,
) -> Result<(Self::AccountId, UnifiedSpendingKey), Self::Error> {
let account = zip32::AccountId::ZERO;
UnifiedSpendingKey::from_seed(&self.network, seed.expose_secret(), account)
.map(|k| (u32::from(account), k))
.map_err(|_| ())
}
fn import_account_hd(
&mut self,
_seed: &SecretVec<u8>,
_account_index: zip32::AccountId,
_birthday: &AccountBirthday,
) -> Result<(Self::Account, UnifiedSpendingKey), Self::Error> {
todo!()
}
fn import_account_ufvk(
&mut self,
_unified_key: &UnifiedFullViewingKey,
_birthday: &AccountBirthday,
_purpose: AccountPurpose,
) -> Result<Self::Account, Self::Error> {
todo!()
}
fn get_next_available_address(
&mut self,
_account: Self::AccountId,
_request: UnifiedAddressRequest,
) -> Result<Option<UnifiedAddress>, Self::Error> {
Ok(None)
}
#[allow(clippy::type_complexity)]
fn put_blocks(
&mut self,
_from_state: &ChainState,
_blocks: Vec<ScannedBlock<Self::AccountId>>,
) -> Result<(), Self::Error> {
Ok(())
}
fn update_chain_tip(&mut self, _tip_height: BlockHeight) -> Result<(), Self::Error> {
Ok(())
}
fn store_decrypted_tx(
&mut self,
_received_tx: DecryptedTransaction<Self::AccountId>,
) -> Result<(), Self::Error> {
Ok(())
}
fn store_transactions_to_be_sent(
&mut self,
_transactions: &[SentTransaction<Self::AccountId>],
) -> Result<(), Self::Error> {
Ok(())
}
fn truncate_to_height(&mut self, _block_height: BlockHeight) -> Result<(), Self::Error> {
Ok(())
}
/// Adds a transparent UTXO received by the wallet to the data store.
fn put_received_transparent_utxo(
&mut self,
_output: &WalletTransparentOutput,
) -> Result<Self::UtxoRef, Self::Error> {
Ok(0)
}
#[cfg(feature = "transparent-inputs")]
fn reserve_next_n_ephemeral_addresses(
&mut self,
_account_id: Self::AccountId,
_n: usize,
) -> Result<Vec<(TransparentAddress, TransparentAddressMetadata)>, Self::Error> {
Err(())
}
fn set_transaction_status(
&mut self,
_txid: TxId,
_status: TransactionStatus,
) -> Result<(), Self::Error> {
Ok(())
}
}
impl WalletCommitmentTrees for MockWalletDb {
type Error = Infallible;
type SaplingShardStore<'a> = MemoryShardStore<sapling::Node, BlockHeight>;
fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::SaplingShardStore<'a>,
{ sapling::NOTE_COMMITMENT_TREE_DEPTH },
SAPLING_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Infallible>>,
{
callback(&mut self.sapling_tree)
}
fn put_sapling_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<sapling::Node>],
) -> Result<(), ShardTreeError<Self::Error>> {
self.with_sapling_tree_mut(|t| {
for (root, i) in roots.iter().zip(0u64..) {
let root_addr =
Address::from_parts(SAPLING_SHARD_HEIGHT.into(), start_index + i);
t.insert(root_addr, *root.root_hash())?;
}
Ok::<_, ShardTreeError<Self::Error>>(())
})?;
Ok(())
}
#[cfg(feature = "orchard")]
type OrchardShardStore<'a> =
MemoryShardStore<orchard::tree::MerkleHashOrchard, BlockHeight>;
#[cfg(feature = "orchard")]
fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::OrchardShardStore<'a>,
{ ORCHARD_SHARD_HEIGHT * 2 },
ORCHARD_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
callback(&mut self.orchard_tree)
}
/// Adds a sequence of note commitment tree subtree roots to the data store.
#[cfg(feature = "orchard")]
fn put_orchard_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
) -> Result<(), ShardTreeError<Self::Error>> {
self.with_orchard_tree_mut(|t| {
for (root, i) in roots.iter().zip(0u64..) {
let root_addr =
Address::from_parts(ORCHARD_SHARD_HEIGHT.into(), start_index + i);
t.insert(root_addr, *root.root_hash())?;
}
Ok::<_, ShardTreeError<Self::Error>>(())
})?;
Ok(())
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,172 @@
use std::hash::Hash;
use ::orchard::{
keys::{FullViewingKey, SpendingKey},
note_encryption::OrchardDomain,
tree::MerkleHashOrchard,
};
use incrementalmerkletree::{Hashable, Level};
use shardtree::error::ShardTreeError;
use zcash_keys::{
address::{Address, UnifiedAddress},
keys::UnifiedSpendingKey,
};
use zcash_note_encryption::try_output_recovery_with_ovk;
use zcash_primitives::transaction::Transaction;
use zcash_protocol::{
consensus::{self, BlockHeight},
memo::MemoBytes,
value::Zatoshis,
ShieldedProtocol,
};
use crate::{
data_api::{
chain::{CommitmentTreeRoot, ScanSummary},
testing::{pool::ShieldedPoolTester, TestState},
DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, WalletSummary,
},
wallet::{Note, ReceivedNote},
};
pub struct OrchardPoolTester;
impl ShieldedPoolTester for OrchardPoolTester {
const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Orchard;
// const MERKLE_TREE_DEPTH: u8 = {orchard::NOTE_COMMITMENT_TREE_DEPTH as u8};
type Sk = SpendingKey;
type Fvk = FullViewingKey;
type MerkleTreeHash = MerkleHashOrchard;
type Note = orchard::note::Note;
fn test_account_fvk<Cache, DbT: WalletRead, P: consensus::Parameters>(
st: &TestState<Cache, DbT, P>,
) -> Self::Fvk {
st.test_account_orchard().unwrap().clone()
}
fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk {
usk.orchard()
}
fn sk(seed: &[u8]) -> Self::Sk {
let mut account = zip32::AccountId::ZERO;
loop {
if let Ok(sk) = SpendingKey::from_zip32_seed(seed, 1, account) {
break sk;
}
account = account.next().unwrap();
}
}
fn sk_to_fvk(sk: &Self::Sk) -> Self::Fvk {
sk.into()
}
fn sk_default_address(sk: &Self::Sk) -> Address {
Self::fvk_default_address(&Self::sk_to_fvk(sk))
}
fn fvk_default_address(fvk: &Self::Fvk) -> Address {
UnifiedAddress::from_receivers(
Some(fvk.address_at(0u32, zip32::Scope::External)),
None,
None,
)
.unwrap()
.into()
}
fn fvks_equal(a: &Self::Fvk, b: &Self::Fvk) -> bool {
a == b
}
fn empty_tree_leaf() -> Self::MerkleTreeHash {
MerkleHashOrchard::empty_leaf()
}
fn empty_tree_root(level: Level) -> Self::MerkleTreeHash {
MerkleHashOrchard::empty_root(level)
}
fn put_subtree_roots<Cache, DbT: WalletRead + WalletCommitmentTrees, P>(
st: &mut TestState<Cache, DbT, P>,
start_index: u64,
roots: &[CommitmentTreeRoot<Self::MerkleTreeHash>],
) -> Result<(), ShardTreeError<<DbT as WalletCommitmentTrees>::Error>> {
st.wallet_mut()
.put_orchard_subtree_roots(start_index, roots)
}
fn next_subtree_index<A: Hash + Eq>(s: &WalletSummary<A>) -> u64 {
s.next_orchard_subtree_index()
}
fn select_spendable_notes<Cache, DbT: InputSource + WalletRead, P>(
st: &TestState<Cache, DbT, P>,
account: <DbT as InputSource>::AccountId,
target_value: Zatoshis,
anchor_height: BlockHeight,
exclude: &[DbT::NoteRef],
) -> Result<Vec<ReceivedNote<DbT::NoteRef, Self::Note>>, <DbT as InputSource>::Error> {
st.wallet()
.select_spendable_notes(
account,
target_value,
&[ShieldedProtocol::Orchard],
anchor_height,
exclude,
)
.map(|n| n.take_orchard())
}
fn decrypted_pool_outputs_count<A>(d_tx: &DecryptedTransaction<'_, A>) -> usize {
d_tx.orchard_outputs().len()
}
fn with_decrypted_pool_memos<A>(
d_tx: &DecryptedTransaction<'_, A>,
mut f: impl FnMut(&MemoBytes),
) {
for output in d_tx.orchard_outputs() {
f(output.memo());
}
}
fn try_output_recovery<P: consensus::Parameters>(
_params: &P,
_: BlockHeight,
tx: &Transaction,
fvk: &Self::Fvk,
) -> Option<(Note, Address, MemoBytes)> {
for action in tx.orchard_bundle().unwrap().actions() {
// Find the output that decrypts with the external OVK
let result = try_output_recovery_with_ovk(
&OrchardDomain::for_action(action),
&fvk.to_ovk(zip32::Scope::External),
action,
action.cv_net(),
&action.encrypted_note().out_ciphertext,
);
if result.is_some() {
return result.map(|(note, addr, memo)| {
(
Note::Orchard(note),
UnifiedAddress::from_receivers(Some(addr), None, None)
.unwrap()
.into(),
MemoBytes::from_bytes(&memo).expect("correct length"),
)
});
}
}
None
}
fn received_note_count(summary: &ScanSummary) -> usize {
summary.received_orchard_note_count()
}
}

View File

@ -0,0 +1,240 @@
use assert_matches::assert_matches;
use incrementalmerkletree::Level;
use rand::RngCore;
use shardtree::error::ShardTreeError;
use std::{cmp::Eq, convert::Infallible, hash::Hash, num::NonZeroU32};
use zcash_keys::{address::Address, keys::UnifiedSpendingKey};
use zcash_primitives::{
block::BlockHash,
transaction::{fees::StandardFeeRule, Transaction},
};
use zcash_protocol::{
consensus::{self, BlockHeight},
memo::{Memo, MemoBytes},
value::Zatoshis,
ShieldedProtocol,
};
use zip321::Payment;
use crate::{
data_api::{
chain::{CommitmentTreeRoot, ScanSummary},
testing::{AddressType, TestBuilder},
wallet::{decrypt_and_store_transaction, input_selection::GreedyInputSelector},
Account as _, DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead,
WalletSummary,
},
decrypt_transaction,
fees::{standard, DustOutputPolicy},
wallet::{Note, NoteId, OvkPolicy, ReceivedNote},
};
use super::{DataStoreFactory, TestCache, TestFvk, TestState};
/// Trait that exposes the pool-specific types and operations necessary to run the
/// single-shielded-pool tests on a given pool.
pub trait ShieldedPoolTester {
const SHIELDED_PROTOCOL: ShieldedProtocol;
type Sk;
type Fvk: TestFvk;
type MerkleTreeHash;
type Note;
fn test_account_fvk<Cache, DbT: WalletRead, P: consensus::Parameters>(
st: &TestState<Cache, DbT, P>,
) -> Self::Fvk;
fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk;
fn sk(seed: &[u8]) -> Self::Sk;
fn sk_to_fvk(sk: &Self::Sk) -> Self::Fvk;
fn sk_default_address(sk: &Self::Sk) -> Address;
fn fvk_default_address(fvk: &Self::Fvk) -> Address;
fn fvks_equal(a: &Self::Fvk, b: &Self::Fvk) -> bool;
fn random_fvk(mut rng: impl RngCore) -> Self::Fvk {
let sk = {
let mut sk_bytes = vec![0; 32];
rng.fill_bytes(&mut sk_bytes);
Self::sk(&sk_bytes)
};
Self::sk_to_fvk(&sk)
}
fn random_address(rng: impl RngCore) -> Address {
Self::fvk_default_address(&Self::random_fvk(rng))
}
fn empty_tree_leaf() -> Self::MerkleTreeHash;
fn empty_tree_root(level: Level) -> Self::MerkleTreeHash;
fn put_subtree_roots<Cache, DbT: WalletRead + WalletCommitmentTrees, P>(
st: &mut TestState<Cache, DbT, P>,
start_index: u64,
roots: &[CommitmentTreeRoot<Self::MerkleTreeHash>],
) -> Result<(), ShardTreeError<<DbT as WalletCommitmentTrees>::Error>>;
fn next_subtree_index<A: Hash + Eq>(s: &WalletSummary<A>) -> u64;
#[allow(clippy::type_complexity)]
fn select_spendable_notes<Cache, DbT: InputSource + WalletRead, P>(
st: &TestState<Cache, DbT, P>,
account: <DbT as InputSource>::AccountId,
target_value: Zatoshis,
anchor_height: BlockHeight,
exclude: &[DbT::NoteRef],
) -> Result<Vec<ReceivedNote<DbT::NoteRef, Self::Note>>, <DbT as InputSource>::Error>;
fn decrypted_pool_outputs_count<A>(d_tx: &DecryptedTransaction<'_, A>) -> usize;
fn with_decrypted_pool_memos<A>(d_tx: &DecryptedTransaction<'_, A>, f: impl FnMut(&MemoBytes));
fn try_output_recovery<P: consensus::Parameters>(
params: &P,
height: BlockHeight,
tx: &Transaction,
fvk: &Self::Fvk,
) -> Option<(Note, Address, MemoBytes)>;
fn received_note_count(summary: &ScanSummary) -> usize;
}
pub fn send_single_step_proposed_transfer<T: ShieldedPoolTester>(
dsf: impl DataStoreFactory,
cache: impl TestCache,
) {
let mut st = TestBuilder::new()
.with_data_store_factory(dsf)
.with_block_cache(cache)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let dfvk = T::test_account_fvk(&st);
// Add funds to the wallet in a single note
let value = Zatoshis::const_from_u64(60000);
let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
st.scan_cached_blocks(h, 1);
// Spendable balance matches total balance
assert_eq!(st.get_total_balance(account.id()), value);
assert_eq!(st.get_spendable_balance(account.id(), 1), value);
assert_eq!(
st.wallet()
.block_max_scanned()
.unwrap()
.unwrap()
.block_height(),
h
);
let to_extsk = T::sk(&[0xf5; 32]);
let to: Address = T::sk_default_address(&to_extsk);
let request = zip321::TransactionRequest::new(vec![Payment::without_memo(
to.to_zcash_address(st.network()),
Zatoshis::const_from_u64(10000),
)])
.unwrap();
let fee_rule = StandardFeeRule::Zip317;
let change_memo = "Test change memo".parse::<Memo>().unwrap();
let change_strategy = standard::SingleOutputChangeStrategy::new(
fee_rule,
Some(change_memo.clone().into()),
T::SHIELDED_PROTOCOL,
);
let input_selector = &GreedyInputSelector::new(change_strategy, DustOutputPolicy::default());
let proposal = st
.propose_transfer(
account.id(),
input_selector,
request,
NonZeroU32::new(1).unwrap(),
)
.unwrap();
let create_proposed_result = st.create_proposed_transactions::<Infallible, _>(
account.usk(),
OvkPolicy::Sender,
&proposal,
);
assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1);
let sent_tx_id = create_proposed_result.unwrap()[0];
// Verify that the sent transaction was stored and that we can decrypt the memos
let tx = st
.wallet()
.get_transaction(sent_tx_id)
.unwrap()
.expect("Created transaction was stored.");
let ufvks = [(account.id(), account.usk().to_unified_full_viewing_key())]
.into_iter()
.collect();
let d_tx = decrypt_transaction(st.network(), h + 1, &tx, &ufvks);
assert_eq!(T::decrypted_pool_outputs_count(&d_tx), 2);
let mut found_tx_change_memo = false;
let mut found_tx_empty_memo = false;
T::with_decrypted_pool_memos(&d_tx, |memo| {
if Memo::try_from(memo).unwrap() == change_memo {
found_tx_change_memo = true
}
if Memo::try_from(memo).unwrap() == Memo::Empty {
found_tx_empty_memo = true
}
});
assert!(found_tx_change_memo);
assert!(found_tx_empty_memo);
// Verify that the stored sent notes match what we're expecting
let sent_note_ids = st
.wallet()
.get_sent_note_ids(&sent_tx_id, T::SHIELDED_PROTOCOL)
.unwrap();
assert_eq!(sent_note_ids.len(), 2);
// The sent memo should be the empty memo for the sent output, and the
// change output's memo should be as specified.
let mut found_sent_change_memo = false;
let mut found_sent_empty_memo = false;
for sent_note_id in sent_note_ids {
match st
.wallet()
.get_memo(sent_note_id)
.expect("Note id is valid")
.as_ref()
{
Some(m) if m == &change_memo => {
found_sent_change_memo = true;
}
Some(m) if m == &Memo::Empty => {
found_sent_empty_memo = true;
}
Some(other) => panic!("Unexpected memo value: {:?}", other),
None => panic!("Memo should not be stored as NULL"),
}
}
assert!(found_sent_change_memo);
assert!(found_sent_empty_memo);
// Check that querying for a nonexistent sent note returns None
assert_matches!(
st.wallet()
.get_memo(NoteId::new(sent_tx_id, T::SHIELDED_PROTOCOL, 12345)),
Ok(None)
);
let tx_history = st.wallet().get_tx_history().unwrap();
assert_eq!(tx_history.len(), 2);
let network = *st.network();
assert_matches!(
decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, None),
Ok(_)
);
}

View File

@ -0,0 +1,152 @@
use std::hash::Hash;
use incrementalmerkletree::{Hashable, Level};
use sapling::{
note_encryption::try_sapling_output_recovery,
zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey},
};
use shardtree::error::ShardTreeError;
use zcash_keys::{address::Address, keys::UnifiedSpendingKey};
use zcash_primitives::transaction::{components::sapling::zip212_enforcement, Transaction};
use zcash_protocol::{
consensus::{self, BlockHeight},
memo::MemoBytes,
value::Zatoshis,
ShieldedProtocol,
};
use zip32::Scope;
use crate::{
data_api::{
chain::{CommitmentTreeRoot, ScanSummary},
DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, WalletSummary,
},
wallet::{Note, ReceivedNote},
};
use super::{pool::ShieldedPoolTester, TestState};
pub struct SaplingPoolTester;
impl ShieldedPoolTester for SaplingPoolTester {
const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Sapling;
// const MERKLE_TREE_DEPTH: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH;
type Sk = ExtendedSpendingKey;
type Fvk = DiversifiableFullViewingKey;
type MerkleTreeHash = sapling::Node;
type Note = sapling::Note;
fn test_account_fvk<Cache, DbT: WalletRead, P: consensus::Parameters>(
st: &TestState<Cache, DbT, P>,
) -> Self::Fvk {
st.test_account_sapling().unwrap().clone()
}
fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk {
usk.sapling()
}
fn sk(seed: &[u8]) -> Self::Sk {
ExtendedSpendingKey::master(seed)
}
fn sk_to_fvk(sk: &Self::Sk) -> Self::Fvk {
sk.to_diversifiable_full_viewing_key()
}
fn sk_default_address(sk: &Self::Sk) -> Address {
sk.default_address().1.into()
}
fn fvk_default_address(fvk: &Self::Fvk) -> Address {
fvk.default_address().1.into()
}
fn fvks_equal(a: &Self::Fvk, b: &Self::Fvk) -> bool {
a.to_bytes() == b.to_bytes()
}
fn empty_tree_leaf() -> Self::MerkleTreeHash {
::sapling::Node::empty_leaf()
}
fn empty_tree_root(level: Level) -> Self::MerkleTreeHash {
::sapling::Node::empty_root(level)
}
fn put_subtree_roots<Cache, DbT: WalletRead + WalletCommitmentTrees, P>(
st: &mut TestState<Cache, DbT, P>,
start_index: u64,
roots: &[CommitmentTreeRoot<Self::MerkleTreeHash>],
) -> Result<(), ShardTreeError<<DbT as WalletCommitmentTrees>::Error>> {
st.wallet_mut()
.put_sapling_subtree_roots(start_index, roots)
}
fn next_subtree_index<A: Hash + Eq>(s: &WalletSummary<A>) -> u64 {
s.next_sapling_subtree_index()
}
fn select_spendable_notes<Cache, DbT: InputSource + WalletRead, P>(
st: &TestState<Cache, DbT, P>,
account: <DbT as InputSource>::AccountId,
target_value: Zatoshis,
anchor_height: BlockHeight,
exclude: &[DbT::NoteRef],
) -> Result<Vec<ReceivedNote<DbT::NoteRef, Self::Note>>, <DbT as InputSource>::Error> {
st.wallet()
.select_spendable_notes(
account,
target_value,
&[ShieldedProtocol::Sapling],
anchor_height,
exclude,
)
.map(|n| n.take_sapling())
}
fn decrypted_pool_outputs_count<A>(d_tx: &DecryptedTransaction<'_, A>) -> usize {
d_tx.sapling_outputs().len()
}
fn with_decrypted_pool_memos<A>(
d_tx: &DecryptedTransaction<'_, A>,
mut f: impl FnMut(&MemoBytes),
) {
for output in d_tx.sapling_outputs() {
f(output.memo());
}
}
fn try_output_recovery<P: consensus::Parameters>(
params: &P,
height: BlockHeight,
tx: &Transaction,
fvk: &Self::Fvk,
) -> Option<(Note, Address, MemoBytes)> {
for output in tx.sapling_bundle().unwrap().shielded_outputs() {
// Find the output that decrypts with the external OVK
let result = try_sapling_output_recovery(
&fvk.to_ovk(Scope::External),
output,
zip212_enforcement(params, height),
);
if result.is_some() {
return result.map(|(note, addr, memo)| {
(
Note::Sapling(note),
addr.into(),
MemoBytes::from_bytes(&memo).expect("correct length"),
)
});
}
}
None
}
fn received_note_count(summary: &ScanSummary) -> usize {
summary.received_sapling_note_count()
}
}

View File

@ -38,7 +38,7 @@ pub struct DecryptedOutput<Note, AccountId> {
transfer_type: TransferType,
}
impl<Note, AccountId: Copy> DecryptedOutput<Note, AccountId> {
impl<Note, AccountId> DecryptedOutput<Note, AccountId> {
pub fn new(
index: usize,
note: Note,

View File

@ -6,7 +6,7 @@ use arti_client::{config::TorClientConfigBuilder, TorClient};
use tor_rtcompat::PreferredRuntime;
use tracing::debug;
#[cfg(feature = "lightwalletd-tonic")]
#[cfg(feature = "tor-lightwalletd-tonic")]
mod grpc;
pub mod http;

View File

@ -79,6 +79,7 @@ document-features.workspace = true
maybe-rayon.workspace = true
[dev-dependencies]
ambassador.workspace = true
assert_matches.workspace = true
bls12_381.workspace = true
incrementalmerkletree = { workspace = true, features = ["test-dependencies"] }

View File

@ -322,10 +322,12 @@ where
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use crate::{testing, wallet::sapling::tests::SaplingPoolTester};
use zcash_client_backend::data_api::testing::sapling::SaplingPoolTester;
use crate::testing;
#[cfg(feature = "orchard")]
use crate::wallet::orchard::tests::OrchardPoolTester;
use zcash_client_backend::data_api::testing::orchard::OrchardPoolTester;
#[test]
fn valid_chain_states_sapling() {

View File

@ -74,7 +74,7 @@ use zip32::fingerprint::SeedFingerprint;
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
#[cfg(not(feature = "orchard"))]
#[cfg(any(feature = "test-dependencies", not(feature = "orchard")))]
use zcash_protocol::PoolType;
#[cfg(feature = "orchard")]
@ -98,6 +98,9 @@ use maybe_rayon::{
slice::ParallelSliceMut,
};
#[cfg(any(test, feature = "test-dependencies"))]
use zcash_client_backend::data_api::testing::TransactionSummary;
/// `maybe-rayon` doesn't provide this as a fallback, so we have to.
#[cfg(not(feature = "multicore"))]
trait ParallelSliceMut<T> {
@ -264,28 +267,36 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
&self,
account: AccountId,
target_value: NonNegativeAmount,
_sources: &[ShieldedProtocol],
sources: &[ShieldedProtocol],
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<SpendableNotes<Self::NoteRef>, Self::Error> {
Ok(SpendableNotes::new(
wallet::sapling::select_spendable_sapling_notes(
self.conn.borrow(),
&self.params,
account,
target_value,
anchor_height,
exclude,
)?,
if sources.contains(&ShieldedProtocol::Sapling) {
wallet::sapling::select_spendable_sapling_notes(
self.conn.borrow(),
&self.params,
account,
target_value,
anchor_height,
exclude,
)?
} else {
vec![]
},
#[cfg(feature = "orchard")]
wallet::orchard::select_spendable_orchard_notes(
self.conn.borrow(),
&self.params,
account,
target_value,
anchor_height,
exclude,
)?,
if sources.contains(&ShieldedProtocol::Orchard) {
wallet::orchard::select_spendable_orchard_notes(
self.conn.borrow(),
&self.params,
account,
target_value,
anchor_height,
exclude,
)?
} else {
vec![]
},
))
}
@ -603,6 +614,36 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for W
Ok(iter.collect())
}
#[cfg(any(test, feature = "test-dependencies"))]
fn get_tx_history(&self) -> Result<Vec<TransactionSummary<Self::AccountId>>, Self::Error> {
wallet::testing::get_tx_history(self.conn.borrow())
}
#[cfg(any(test, feature = "test-dependencies"))]
fn get_sent_note_ids(
&self,
txid: &TxId,
protocol: ShieldedProtocol,
) -> Result<Vec<NoteId>, Self::Error> {
use crate::wallet::pool_code;
use rusqlite::named_params;
let mut stmt_sent_notes = self.conn.borrow().prepare(
"SELECT output_index
FROM sent_notes
JOIN transactions ON transactions.id_tx = sent_notes.tx
WHERE transactions.txid = :txid
AND sent_notes.output_pool = :pool_code",
)?;
stmt_sent_notes
.query(named_params![":txid": txid.as_ref(), ":pool_code": pool_code(PoolType::Shielded(protocol))])
.unwrap()
.mapped(|row| Ok(NoteId::new(*txid, protocol, row.get(0)?)))
.collect::<Result<Vec<_>, _>>()
.map_err(SqliteClientError::from)
}
}
impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P> {
@ -1682,34 +1723,35 @@ extern crate assert_matches;
mod tests {
use secrecy::{ExposeSecret, Secret, SecretVec};
use zcash_client_backend::data_api::{
chain::ChainState, Account, AccountBirthday, AccountPurpose, AccountSource, WalletRead,
WalletWrite,
chain::ChainState,
testing::{TestBuilder, TestState},
Account, AccountBirthday, AccountPurpose, AccountSource, WalletRead, WalletWrite,
};
use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey};
use zcash_primitives::block::BlockHash;
use zcash_protocol::consensus;
use crate::{
error::SqliteClientError,
testing::{TestBuilder, TestState},
AccountId, DEFAULT_UA_REQUEST,
error::SqliteClientError, testing::db::TestDbFactory, AccountId, DEFAULT_UA_REQUEST,
};
#[cfg(feature = "unstable")]
use {
crate::testing::AddressType, zcash_client_backend::keys::sapling,
zcash_client_backend::keys::sapling,
zcash_primitives::transaction::components::amount::NonNegativeAmount,
};
#[test]
fn validate_seed() {
let st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().unwrap();
assert!({
st.wallet()
.validate_seed(account.account_id(), st.test_seed().unwrap())
.validate_seed(account.id(), st.test_seed().unwrap())
.unwrap()
});
@ -1724,7 +1766,7 @@ mod tests {
// check that passing an invalid seed results in a failure
assert!({
!st.wallet()
.validate_seed(account.account_id(), &SecretVec::new(vec![1u8; 32]))
.validate_seed(account.id(), &SecretVec::new(vec![1u8; 32]))
.unwrap()
});
}
@ -1732,33 +1774,29 @@ mod tests {
#[test]
pub(crate) fn get_next_available_address() {
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let current_addr = st
.wallet()
.get_current_address(account.account_id())
.unwrap();
let current_addr = st.wallet().get_current_address(account.id()).unwrap();
assert!(current_addr.is_some());
let addr2 = st
.wallet_mut()
.get_next_available_address(account.account_id(), DEFAULT_UA_REQUEST)
.get_next_available_address(account.id(), DEFAULT_UA_REQUEST)
.unwrap();
assert!(addr2.is_some());
assert_ne!(current_addr, addr2);
let addr2_cur = st
.wallet()
.get_current_address(account.account_id())
.unwrap();
let addr2_cur = st.wallet().get_current_address(account.id()).unwrap();
assert_eq!(addr2, addr2_cur);
}
#[test]
pub(crate) fn import_account_hd_0() {
let st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.set_account_index(zip32::AccountId::ZERO)
.build();
@ -1769,10 +1807,12 @@ mod tests {
#[test]
pub(crate) fn import_account_hd_1_then_2() {
let mut st = TestBuilder::new().build();
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.build();
let birthday = AccountBirthday::from_parts(
ChainState::empty(st.wallet().params.sapling.unwrap() - 1, BlockHash([0; 32])),
ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
@ -1797,15 +1837,19 @@ mod tests {
AccountSource::Derived { seed_fingerprint: _, account_index } if account_index == zip32_index_2);
}
fn check_collisions<C>(
st: &mut TestState<C>,
fn check_collisions<C, DbT: WalletWrite, P: consensus::Parameters>(
st: &mut TestState<C, DbT, P>,
ufvk: &UnifiedFullViewingKey,
birthday: &AccountBirthday,
existing_id: AccountId,
) {
is_account_collision: impl Fn(&DbT::Error) -> bool,
) where
DbT::Account: core::fmt::Debug,
{
assert_matches!(
st.wallet_mut().import_account_ufvk(ufvk, birthday, AccountPurpose::Spending),
Err(SqliteClientError::AccountCollision(id)) if id == existing_id);
st.wallet_mut()
.import_account_ufvk(ufvk, birthday, AccountPurpose::Spending),
Err(e) if is_account_collision(&e)
);
// Remove the transparent component so that we don't have a match on the full UFVK.
// That should still produce an AccountCollision error.
@ -1820,8 +1864,13 @@ mod tests {
)
.unwrap();
assert_matches!(
st.wallet_mut().import_account_ufvk(&subset_ufvk, birthday, AccountPurpose::Spending),
Err(SqliteClientError::AccountCollision(id)) if id == existing_id);
st.wallet_mut().import_account_ufvk(
&subset_ufvk,
birthday,
AccountPurpose::Spending
),
Err(e) if is_account_collision(&e)
);
}
// Remove the Orchard component so that we don't have a match on the full UFVK.
@ -1837,17 +1886,24 @@ mod tests {
)
.unwrap();
assert_matches!(
st.wallet_mut().import_account_ufvk(&subset_ufvk, birthday, AccountPurpose::Spending),
Err(SqliteClientError::AccountCollision(id)) if id == existing_id);
st.wallet_mut().import_account_ufvk(
&subset_ufvk,
birthday,
AccountPurpose::Spending
),
Err(e) if is_account_collision(&e)
);
}
}
#[test]
pub(crate) fn import_account_hd_1_then_conflicts() {
let mut st = TestBuilder::new().build();
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.build();
let birthday = AccountBirthday::from_parts(
ChainState::empty(st.wallet().params.sapling.unwrap() - 1, BlockHash([0; 32])),
ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
@ -1864,23 +1920,29 @@ mod tests {
st.wallet_mut().import_account_hd(&seed, zip32_index_1, &birthday),
Err(SqliteClientError::AccountCollision(id)) if id == first_account.id());
check_collisions(&mut st, ufvk, &birthday, first_account.id());
check_collisions(
&mut st,
ufvk,
&birthday,
|e| matches!(e, SqliteClientError::AccountCollision(id) if *id == first_account.id()),
);
}
#[test]
pub(crate) fn import_account_ufvk_then_conflicts() {
let mut st = TestBuilder::new().build();
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.build();
let birthday = AccountBirthday::from_parts(
ChainState::empty(st.wallet().params.sapling.unwrap() - 1, BlockHash([0; 32])),
ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
let seed = Secret::new(vec![0u8; 32]);
let zip32_index_0 = zip32::AccountId::ZERO;
let usk =
UnifiedSpendingKey::from_seed(&st.wallet().params, seed.expose_secret(), zip32_index_0)
.unwrap();
let usk = UnifiedSpendingKey::from_seed(st.network(), seed.expose_secret(), zip32_index_0)
.unwrap();
let ufvk = usk.to_unified_full_viewing_key();
let account = st
@ -1888,8 +1950,8 @@ mod tests {
.import_account_ufvk(&ufvk, &birthday, AccountPurpose::Spending)
.unwrap();
assert_eq!(
ufvk.encode(&st.wallet().params),
account.ufvk().unwrap().encode(&st.wallet().params)
ufvk.encode(st.network()),
account.ufvk().unwrap().encode(st.network())
);
assert_matches!(
@ -1903,15 +1965,22 @@ mod tests {
st.wallet_mut().import_account_hd(&seed, zip32_index_0, &birthday),
Err(SqliteClientError::AccountCollision(id)) if id == account.id());
check_collisions(&mut st, &ufvk, &birthday, account.id());
check_collisions(
&mut st,
&ufvk,
&birthday,
|e| matches!(e, SqliteClientError::AccountCollision(id) if *id == account.id()),
);
}
#[test]
pub(crate) fn create_account_then_conflicts() {
let mut st = TestBuilder::new().build();
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.build();
let birthday = AccountBirthday::from_parts(
ChainState::empty(st.wallet().params.sapling.unwrap() - 1, BlockHash([0; 32])),
ChainState::empty(st.network().sapling.unwrap() - 1, BlockHash([0; 32])),
None,
);
@ -1925,25 +1994,30 @@ mod tests {
st.wallet_mut().import_account_hd(&seed, zip32_index_0, &birthday),
Err(SqliteClientError::AccountCollision(id)) if id == seed_based.0);
check_collisions(&mut st, ufvk, &birthday, seed_based.0);
check_collisions(
&mut st,
ufvk,
&birthday,
|e| matches!(e, SqliteClientError::AccountCollision(id) if *id == seed_based.0),
);
}
#[cfg(feature = "transparent-inputs")]
#[test]
fn transparent_receivers() {
// Add an account to the wallet.
use crate::testing::BlockCache;
let st = TestBuilder::new()
.with_block_cache()
.with_data_store_factory(TestDbFactory)
.with_block_cache(BlockCache::new())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().unwrap();
let ufvk = account.usk().to_unified_full_viewing_key();
let (taddr, _) = account.usk().default_transparent_address();
let receivers = st
.wallet()
.get_transparent_receivers(account.account_id())
.unwrap();
let receivers = st.wallet().get_transparent_receivers(account.id()).unwrap();
// The receiver for the default UA should be in the set.
assert!(receivers.contains_key(
@ -1961,10 +2035,16 @@ mod tests {
#[cfg(feature = "unstable")]
#[test]
pub(crate) fn fsblockdb_api() {
use zcash_primitives::consensus::NetworkConstants;
use zcash_client_backend::data_api::testing::AddressType;
use zcash_primitives::zip32;
use zcash_protocol::consensus::NetworkConstants;
let mut st = TestBuilder::new().with_fs_block_cache().build();
use crate::testing::FsBlockCache;
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.with_block_cache(FsBlockCache::new())
.build();
// The BlockMeta DB starts off empty.
assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
@ -1972,7 +2052,7 @@ mod tests {
// Generate some fake CompactBlocks.
let seed = [0u8; 32];
let hd_account_index = zip32::AccountId::ZERO;
let extsk = sapling::spending_key(&seed, st.wallet().params.coin_type(), hd_account_index);
let extsk = sapling::spending_key(&seed, st.network().coin_type(), hd_account_index);
let dfvk = extsk.to_diversifiable_full_viewing_key();
let (h1, meta1, _) = st.generate_next_block(
&dfvk,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,172 @@
use ambassador::Delegate;
use rusqlite::Connection;
use std::collections::HashMap;
use std::num::NonZeroU32;
use tempfile::NamedTempFile;
use rusqlite::{self};
use secrecy::SecretVec;
use shardtree::{error::ShardTreeError, ShardTree};
use zip32::fingerprint::SeedFingerprint;
use zcash_client_backend::{
data_api::{
chain::{ChainState, CommitmentTreeRoot},
scanning::ScanRange,
testing::{DataStoreFactory, Reset, TestState},
*,
},
keys::UnifiedFullViewingKey,
wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput},
ShieldedProtocol,
};
use zcash_keys::{
address::UnifiedAddress,
keys::{UnifiedAddressRequest, UnifiedSpendingKey},
};
use zcash_primitives::{
block::BlockHash,
transaction::{components::amount::NonNegativeAmount, Transaction, TxId},
};
use zcash_protocol::{consensus::BlockHeight, local_consensus::LocalNetwork, memo::Memo};
use crate::{error::SqliteClientError, wallet::init::init_wallet_db, AccountId, WalletDb};
#[cfg(feature = "transparent-inputs")]
use {
crate::TransparentAddressMetadata,
core::ops::Range,
zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint},
};
#[derive(Delegate)]
#[delegate(InputSource, target = "wallet_db")]
#[delegate(WalletRead, target = "wallet_db")]
#[delegate(WalletWrite, target = "wallet_db")]
#[delegate(WalletCommitmentTrees, target = "wallet_db")]
pub(crate) struct TestDb {
wallet_db: WalletDb<Connection, LocalNetwork>,
data_file: NamedTempFile,
}
impl TestDb {
fn from_parts(wallet_db: WalletDb<Connection, LocalNetwork>, data_file: NamedTempFile) -> Self {
Self {
wallet_db,
data_file,
}
}
pub(crate) fn db(&self) -> &WalletDb<Connection, LocalNetwork> {
&self.wallet_db
}
pub(crate) fn db_mut(&mut self) -> &mut WalletDb<Connection, LocalNetwork> {
&mut self.wallet_db
}
pub(crate) fn conn(&self) -> &Connection {
&self.wallet_db.conn
}
pub(crate) fn conn_mut(&mut self) -> &mut Connection {
&mut self.wallet_db.conn
}
pub(crate) fn take_data_file(self) -> NamedTempFile {
self.data_file
}
/// Dump the schema and contents of the given database table, in
/// sqlite3 ".dump" format. The name of the table must be a static
/// string. This assumes that `sqlite3` is on your path and that it
/// invokes a compatible version of sqlite3.
///
/// # Panics
///
/// Panics if `name` contains characters outside `[a-zA-Z_]`.
#[allow(dead_code)]
#[cfg(feature = "unstable")]
pub(crate) fn dump_table(&self, name: &'static str) {
assert!(name.chars().all(|c| c.is_ascii_alphabetic() || c == '_'));
unsafe {
run_sqlite3(self.data_file.path(), &format!(r#".dump "{name}""#));
}
}
/// Print the results of an arbitrary sqlite3 command (with "-safe"
/// and "-readonly" flags) to stderr. This is completely insecure and
/// should not be exposed in production. Use of the "-safe" and
/// "-readonly" flags is intended only to limit *accidental* misuse.
/// The output is unfiltered, and control codes could mess up your
/// terminal. This assumes that `sqlite3` is on your path and that it
/// invokes a compatible version of sqlite3.
#[allow(dead_code)]
#[cfg(feature = "unstable")]
pub(crate) unsafe fn run_sqlite3(&self, command: &str) {
run_sqlite3(self.data_file.path(), command)
}
}
#[cfg(feature = "unstable")]
use std::{ffi::OsStr, process::Command};
// See the doc comment for `TestState::run_sqlite3` above.
//
// - `db_path` is the path to the database file.
// - `command` may contain newlines.
#[allow(dead_code)]
#[cfg(feature = "unstable")]
unsafe fn run_sqlite3<S: AsRef<OsStr>>(db_path: S, command: &str) {
let output = Command::new("sqlite3")
.arg(db_path)
.arg("-safe")
.arg("-readonly")
.arg(command)
.output()
.expect("failed to execute sqlite3 process");
eprintln!(
"{}\n------\n{}",
command,
String::from_utf8_lossy(&output.stdout)
);
if !output.stderr.is_empty() {
eprintln!(
"------ stderr:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
eprintln!("------");
}
pub(crate) struct TestDbFactory;
impl DataStoreFactory for TestDbFactory {
type Error = ();
type AccountId = AccountId;
type Account = crate::wallet::Account;
type DsError = SqliteClientError;
type DataStore = TestDb;
fn new_data_store(&self, network: LocalNetwork) -> Result<Self::DataStore, Self::Error> {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), network).unwrap();
init_wallet_db(&mut db_data, None).unwrap();
Ok(TestDb::from_parts(db_data, data_file))
}
}
impl Reset for TestDb {
type Handle = NamedTempFile;
fn reset<C>(st: &mut TestState<C, Self, LocalNetwork>) -> NamedTempFile {
let network = *st.network();
let old_db = std::mem::replace(
st.wallet_mut(),
TestDbFactory.new_data_store(network).unwrap(),
);
old_db.take_data_file()
}
}

File diff suppressed because it is too large Load Diff

View File

@ -220,7 +220,9 @@ impl Account {
}
}
impl zcash_client_backend::data_api::Account<AccountId> for Account {
impl zcash_client_backend::data_api::Account for Account {
type AccountId = AccountId;
fn id(&self) -> AccountId {
self.account_id
}
@ -3168,17 +3170,99 @@ pub(crate) fn prune_nullifier_map(
Ok(())
}
#[cfg(any(test, feature = "test-dependencies"))]
pub mod testing {
use incrementalmerkletree::Position;
use zcash_client_backend::data_api::testing::TransactionSummary;
use zcash_primitives::transaction::TxId;
use zcash_protocol::{
consensus::BlockHeight,
value::{ZatBalance, Zatoshis},
ShieldedProtocol,
};
use crate::{error::SqliteClientError, AccountId};
pub(crate) fn get_tx_history(
conn: &rusqlite::Connection,
) -> Result<Vec<TransactionSummary<AccountId>>, SqliteClientError> {
let mut stmt = conn.prepare_cached(
"SELECT *
FROM v_transactions
ORDER BY mined_height DESC, tx_index DESC",
)?;
let results = stmt
.query_and_then::<TransactionSummary<AccountId>, SqliteClientError, _, _>([], |row| {
Ok(TransactionSummary::new(
AccountId(row.get("account_id")?),
TxId::from_bytes(row.get("txid")?),
row.get::<_, Option<u32>>("expiry_height")?
.map(BlockHeight::from),
row.get::<_, Option<u32>>("mined_height")?
.map(BlockHeight::from),
ZatBalance::from_i64(row.get("account_balance_delta")?)?,
row.get::<_, Option<i64>>("fee_paid")?
.map(Zatoshis::from_nonnegative_i64)
.transpose()?,
row.get("spent_note_count")?,
row.get("has_change")?,
row.get("sent_note_count")?,
row.get("received_note_count")?,
row.get("memo_count")?,
row.get("expired_unmined")?,
row.get("is_shielding")?,
))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(results)
}
/// Returns a vector of transaction summaries
#[allow(dead_code)] // used only for tests that are flagged off by default
pub(crate) fn get_checkpoint_history(
conn: &rusqlite::Connection,
) -> Result<Vec<(BlockHeight, ShieldedProtocol, Option<Position>)>, SqliteClientError> {
let mut stmt = conn.prepare_cached(
"SELECT checkpoint_id, 2 AS pool, position FROM sapling_tree_checkpoints
UNION
SELECT checkpoint_id, 3 AS pool, position FROM orchard_tree_checkpoints
ORDER BY checkpoint_id",
)?;
let results = stmt
.query_and_then::<_, SqliteClientError, _, _>([], |row| {
Ok((
BlockHeight::from(row.get::<_, u32>(0)?),
match row.get::<_, i64>(1)? {
2 => ShieldedProtocol::Sapling,
3 => ShieldedProtocol::Orchard,
_ => unreachable!(),
},
row.get::<_, Option<u64>>(2)?.map(Position::from),
))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(results)
}
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU32;
use sapling::zip32::ExtendedSpendingKey;
use secrecy::{ExposeSecret, SecretVec};
use zcash_client_backend::data_api::{AccountSource, WalletRead};
use zcash_client_backend::data_api::{
testing::{AddressType, DataStoreFactory, FakeCompactOutput, TestBuilder, TestState},
Account as _, AccountSource, WalletRead, WalletWrite,
};
use zcash_primitives::{block::BlockHash, transaction::components::amount::NonNegativeAmount};
use crate::{
testing::{AddressType, BlockCache, FakeCompactOutput, TestBuilder, TestState},
testing::{db::TestDbFactory, BlockCache},
AccountId,
};
@ -3187,6 +3271,7 @@ mod tests {
#[test]
fn empty_database_has_no_balance() {
let st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().unwrap();
@ -3203,27 +3288,23 @@ mod tests {
);
// The default address is set for the test account
assert_matches!(
st.wallet().get_current_address(account.account_id()),
Ok(Some(_))
);
assert_matches!(st.wallet().get_current_address(account.id()), Ok(Some(_)));
// No default address is set for an un-initialized account
assert_matches!(
st.wallet()
.get_current_address(AccountId(account.account_id().0 + 1)),
.get_current_address(AccountId(account.id().0 + 1)),
Ok(None)
);
}
#[test]
fn get_default_account_index() {
use crate::testing::TestBuilder;
let st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().unwrap().account_id();
let account_id = st.test_account().unwrap().id();
let account_parameters = st.wallet().get_account(account_id).unwrap().unwrap();
let expected_account_index = zip32::AccountId::try_from(0).unwrap();
@ -3235,10 +3316,8 @@ mod tests {
#[test]
fn get_account_ids() {
use crate::testing::TestBuilder;
use zcash_client_backend::data_api::WalletWrite;
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
@ -3254,12 +3333,17 @@ mod tests {
#[test]
fn block_fully_scanned() {
check_block_fully_scanned(TestDbFactory)
}
fn check_block_fully_scanned<DsF: DataStoreFactory>(dsf: DsF) {
let mut st = TestBuilder::new()
.with_block_cache()
.with_data_store_factory(dsf)
.with_block_cache(BlockCache::new())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let block_fully_scanned = |st: &TestState<BlockCache>| {
let block_fully_scanned = |st: &TestState<_, DsF::DataStore, _>| {
st.wallet()
.block_fully_scanned()
.unwrap()
@ -3314,13 +3398,14 @@ mod tests {
#[test]
fn test_account_birthday() {
let st = TestBuilder::new()
.with_block_cache()
.with_data_store_factory(TestDbFactory)
.with_block_cache(BlockCache::new())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account_id = st.test_account().unwrap().account_id();
let account_id = st.test_account().unwrap().id();
assert_matches!(
account_birthday(&st.wallet().conn, account_id),
account_birthday(st.wallet().conn(), account_id),
Ok(birthday) if birthday == st.sapling_activation_height()
)
}

View File

@ -1081,17 +1081,16 @@ mod tests {
Marking, Position, Retention,
};
use shardtree::ShardTree;
use zcash_client_backend::data_api::chain::CommitmentTreeRoot;
use zcash_client_backend::data_api::{
chain::CommitmentTreeRoot,
testing::{pool::ShieldedPoolTester, sapling::SaplingPoolTester},
};
use zcash_primitives::consensus::{BlockHeight, Network};
use super::SqliteShardStore;
use crate::{
testing::pool::ShieldedPoolTester,
wallet::{init::init_wallet_db, sapling::tests::SaplingPoolTester},
WalletDb,
};
use crate::{testing::pool::ShieldedPoolPersistence, wallet::init::init_wallet_db, WalletDb};
fn new_tree<T: ShieldedPoolTester>(
fn new_tree<T: ShieldedPoolTester + ShieldedPoolPersistence>(
m: usize,
) -> ShardTree<SqliteShardStore<rusqlite::Connection, String, 3>, 4, 3> {
let data_file = NamedTempFile::new().unwrap();
@ -1108,7 +1107,7 @@ mod tests {
#[cfg(feature = "orchard")]
mod orchard {
use super::new_tree;
use crate::wallet::orchard::tests::OrchardPoolTester;
use zcash_client_backend::data_api::testing::orchard::OrchardPoolTester;
#[test]
fn append() {
@ -1191,7 +1190,7 @@ mod tests {
put_shard_roots::<SaplingPoolTester>()
}
fn put_shard_roots<T: ShieldedPoolTester>() {
fn put_shard_roots<T: ShieldedPoolTester + ShieldedPoolPersistence>() {
let data_file = NamedTempFile::new().unwrap();
let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap();
data_file.keep().unwrap();

View File

@ -418,6 +418,7 @@ mod tests {
use zcash_client_backend::{
address::Address,
data_api::testing::TestBuilder,
encoding::{encode_extended_full_viewing_key, encode_payment_address},
keys::{sapling, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
};
@ -429,7 +430,7 @@ mod tests {
zip32::AccountId,
};
use crate::{testing::TestBuilder, wallet::db, WalletDb, UA_TRANSPARENT};
use crate::{testing::db::TestDbFactory, wallet::db, WalletDb, UA_TRANSPARENT};
use super::init_wallet_db;
@ -453,7 +454,9 @@ mod tests {
#[test]
fn verify_schema() {
let st = TestBuilder::new().build();
let st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.build();
use regex::Regex;
let re = Regex::new(r"\s+").unwrap();
@ -489,7 +492,7 @@ mod tests {
db::TABLE_TX_RETRIEVAL_QUEUE,
];
let rows = describe_tables(&st.wallet().conn).unwrap();
let rows = describe_tables(&st.wallet().db().conn).unwrap();
assert_eq!(rows.len(), expected_tables.len());
for (actual, expected) in rows.iter().zip(expected_tables.iter()) {
assert_eq!(
@ -515,6 +518,7 @@ mod tests {
];
let mut indices_query = st
.wallet()
.db()
.conn
.prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND sql != '' ORDER BY tbl_name, name")
.unwrap();
@ -530,12 +534,12 @@ mod tests {
}
let expected_views = vec![
db::view_orchard_shard_scan_ranges(&st.network()),
db::view_orchard_shard_scan_ranges(st.network()),
db::view_orchard_shard_unscanned_ranges(),
db::VIEW_ORCHARD_SHARDS_SCAN_STATE.to_owned(),
db::VIEW_RECEIVED_OUTPUT_SPENDS.to_owned(),
db::VIEW_RECEIVED_OUTPUTS.to_owned(),
db::view_sapling_shard_scan_ranges(&st.network()),
db::view_sapling_shard_scan_ranges(st.network()),
db::view_sapling_shard_unscanned_ranges(),
db::VIEW_SAPLING_SHARDS_SCAN_STATE.to_owned(),
db::VIEW_TRANSACTIONS.to_owned(),
@ -544,6 +548,7 @@ mod tests {
let mut views_query = st
.wallet()
.db()
.conn
.prepare("SELECT sql FROM sqlite_schema WHERE type = 'view' ORDER BY tbl_name")
.unwrap();

View File

@ -387,182 +387,12 @@ pub(crate) fn mark_orchard_note_spent(
#[cfg(test)]
pub(crate) mod tests {
use incrementalmerkletree::{Hashable, Level};
use orchard::{
keys::{FullViewingKey, SpendingKey},
note_encryption::OrchardDomain,
tree::MerkleHashOrchard,
};
use shardtree::error::ShardTreeError;
use zcash_client_backend::{
data_api::{
chain::CommitmentTreeRoot, DecryptedTransaction, WalletCommitmentTrees, WalletSummary,
},
wallet::{Note, ReceivedNote},
};
use zcash_keys::{
address::{Address, UnifiedAddress},
keys::UnifiedSpendingKey,
};
use zcash_note_encryption::try_output_recovery_with_ovk;
use zcash_primitives::transaction::Transaction;
use zcash_protocol::{consensus::BlockHeight, memo::MemoBytes, ShieldedProtocol};
use super::select_spendable_orchard_notes;
use crate::{
error::SqliteClientError,
testing::{
self,
pool::{OutputRecoveryError, ShieldedPoolTester},
TestState,
},
wallet::{commitment_tree, sapling::tests::SaplingPoolTester},
ORCHARD_TABLES_PREFIX,
use zcash_client_backend::data_api::testing::{
orchard::OrchardPoolTester, sapling::SaplingPoolTester,
};
pub(crate) struct OrchardPoolTester;
impl ShieldedPoolTester for OrchardPoolTester {
const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Orchard;
const TABLES_PREFIX: &'static str = ORCHARD_TABLES_PREFIX;
// const MERKLE_TREE_DEPTH: u8 = {orchard::NOTE_COMMITMENT_TREE_DEPTH as u8};
type Sk = SpendingKey;
type Fvk = FullViewingKey;
type MerkleTreeHash = MerkleHashOrchard;
type Note = orchard::note::Note;
fn test_account_fvk<Cache>(st: &TestState<Cache>) -> Self::Fvk {
st.test_account_orchard().unwrap()
}
fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk {
usk.orchard()
}
fn sk(seed: &[u8]) -> Self::Sk {
let mut account = zip32::AccountId::ZERO;
loop {
if let Ok(sk) = SpendingKey::from_zip32_seed(seed, 1, account) {
break sk;
}
account = account.next().unwrap();
}
}
fn sk_to_fvk(sk: &Self::Sk) -> Self::Fvk {
sk.into()
}
fn sk_default_address(sk: &Self::Sk) -> Address {
Self::fvk_default_address(&Self::sk_to_fvk(sk))
}
fn fvk_default_address(fvk: &Self::Fvk) -> Address {
UnifiedAddress::from_receivers(
Some(fvk.address_at(0u32, zip32::Scope::External)),
None,
None,
)
.unwrap()
.into()
}
fn fvks_equal(a: &Self::Fvk, b: &Self::Fvk) -> bool {
a == b
}
fn empty_tree_leaf() -> Self::MerkleTreeHash {
MerkleHashOrchard::empty_leaf()
}
fn empty_tree_root(level: Level) -> Self::MerkleTreeHash {
MerkleHashOrchard::empty_root(level)
}
fn put_subtree_roots<Cache>(
st: &mut TestState<Cache>,
start_index: u64,
roots: &[CommitmentTreeRoot<Self::MerkleTreeHash>],
) -> Result<(), ShardTreeError<commitment_tree::Error>> {
st.wallet_mut()
.put_orchard_subtree_roots(start_index, roots)
}
fn next_subtree_index(s: &WalletSummary<crate::AccountId>) -> u64 {
s.next_orchard_subtree_index()
}
fn select_spendable_notes<Cache>(
st: &TestState<Cache>,
account: crate::AccountId,
target_value: zcash_protocol::value::Zatoshis,
anchor_height: BlockHeight,
exclude: &[crate::ReceivedNoteId],
) -> Result<Vec<ReceivedNote<crate::ReceivedNoteId, orchard::note::Note>>, SqliteClientError>
{
select_spendable_orchard_notes(
&st.wallet().conn,
&st.wallet().params,
account,
target_value,
anchor_height,
exclude,
)
}
fn decrypted_pool_outputs_count(
d_tx: &DecryptedTransaction<'_, crate::AccountId>,
) -> usize {
d_tx.orchard_outputs().len()
}
fn with_decrypted_pool_memos(
d_tx: &DecryptedTransaction<'_, crate::AccountId>,
mut f: impl FnMut(&MemoBytes),
) {
for output in d_tx.orchard_outputs() {
f(output.memo());
}
}
fn try_output_recovery<Cache>(
_: &TestState<Cache>,
_: BlockHeight,
tx: &Transaction,
fvk: &Self::Fvk,
) -> Result<Option<(Note, Address, MemoBytes)>, OutputRecoveryError> {
for action in tx.orchard_bundle().unwrap().actions() {
// Find the output that decrypts with the external OVK
let result = try_output_recovery_with_ovk(
&OrchardDomain::for_action(action),
&fvk.to_ovk(zip32::Scope::External),
action,
action.cv_net(),
&action.encrypted_note().out_ciphertext,
);
if result.is_some() {
return Ok(result.map(|(note, addr, memo)| {
(
Note::Orchard(note),
UnifiedAddress::from_receivers(Some(addr), None, None)
.unwrap()
.into(),
MemoBytes::from_bytes(&memo).expect("correct length"),
)
}));
}
}
Ok(None)
}
fn received_note_count(
summary: &zcash_client_backend::data_api::chain::ScanSummary,
) -> usize {
summary.received_orchard_note_count()
}
}
use crate::testing::{self};
#[test]
fn send_single_step_proposed_transfer() {

View File

@ -400,175 +400,12 @@ pub(crate) fn put_received_note<T: ReceivedSaplingOutput>(
#[cfg(test)]
pub(crate) mod tests {
use incrementalmerkletree::{Hashable, Level};
use shardtree::error::ShardTreeError;
use zcash_proofs::prover::LocalTxProver;
use zcash_client_backend::data_api::testing::sapling::SaplingPoolTester;
use sapling::{
self,
note_encryption::try_sapling_output_recovery,
prover::{OutputProver, SpendProver},
zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey},
};
use zcash_primitives::{
consensus::BlockHeight,
memo::MemoBytes,
transaction::{
components::{amount::NonNegativeAmount, sapling::zip212_enforcement},
Transaction,
},
zip32::Scope,
};
use crate::testing;
use zcash_client_backend::{
address::Address,
data_api::{
chain::CommitmentTreeRoot, DecryptedTransaction, WalletCommitmentTrees, WalletSummary,
},
keys::UnifiedSpendingKey,
wallet::{Note, ReceivedNote},
ShieldedProtocol,
};
use crate::{
error::SqliteClientError,
testing::{
self,
pool::{OutputRecoveryError, ShieldedPoolTester},
TestState,
},
wallet::{commitment_tree, sapling::select_spendable_sapling_notes},
AccountId, ReceivedNoteId, SAPLING_TABLES_PREFIX,
};
pub(crate) struct SaplingPoolTester;
impl ShieldedPoolTester for SaplingPoolTester {
const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Sapling;
const TABLES_PREFIX: &'static str = SAPLING_TABLES_PREFIX;
// const MERKLE_TREE_DEPTH: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH;
type Sk = ExtendedSpendingKey;
type Fvk = DiversifiableFullViewingKey;
type MerkleTreeHash = sapling::Node;
type Note = sapling::Note;
fn test_account_fvk<Cache>(st: &TestState<Cache>) -> Self::Fvk {
st.test_account_sapling().unwrap()
}
fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk {
usk.sapling()
}
fn sk(seed: &[u8]) -> Self::Sk {
ExtendedSpendingKey::master(seed)
}
fn sk_to_fvk(sk: &Self::Sk) -> Self::Fvk {
sk.to_diversifiable_full_viewing_key()
}
fn sk_default_address(sk: &Self::Sk) -> Address {
sk.default_address().1.into()
}
fn fvk_default_address(fvk: &Self::Fvk) -> Address {
fvk.default_address().1.into()
}
fn fvks_equal(a: &Self::Fvk, b: &Self::Fvk) -> bool {
a.to_bytes() == b.to_bytes()
}
fn empty_tree_leaf() -> Self::MerkleTreeHash {
sapling::Node::empty_leaf()
}
fn empty_tree_root(level: Level) -> Self::MerkleTreeHash {
sapling::Node::empty_root(level)
}
fn put_subtree_roots<Cache>(
st: &mut TestState<Cache>,
start_index: u64,
roots: &[CommitmentTreeRoot<Self::MerkleTreeHash>],
) -> Result<(), ShardTreeError<commitment_tree::Error>> {
st.wallet_mut()
.put_sapling_subtree_roots(start_index, roots)
}
fn next_subtree_index(s: &WalletSummary<AccountId>) -> u64 {
s.next_sapling_subtree_index()
}
fn select_spendable_notes<Cache>(
st: &TestState<Cache>,
account: AccountId,
target_value: NonNegativeAmount,
anchor_height: BlockHeight,
exclude: &[ReceivedNoteId],
) -> Result<Vec<ReceivedNote<ReceivedNoteId, Self::Note>>, SqliteClientError> {
select_spendable_sapling_notes(
&st.wallet().conn,
&st.wallet().params,
account,
target_value,
anchor_height,
exclude,
)
}
fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, AccountId>) -> usize {
d_tx.sapling_outputs().len()
}
fn with_decrypted_pool_memos(
d_tx: &DecryptedTransaction<'_, AccountId>,
mut f: impl FnMut(&MemoBytes),
) {
for output in d_tx.sapling_outputs() {
f(output.memo());
}
}
fn try_output_recovery<Cache>(
st: &TestState<Cache>,
height: BlockHeight,
tx: &Transaction,
fvk: &Self::Fvk,
) -> Result<Option<(Note, Address, MemoBytes)>, OutputRecoveryError> {
for output in tx.sapling_bundle().unwrap().shielded_outputs() {
// Find the output that decrypts with the external OVK
let result = try_sapling_output_recovery(
&fvk.to_ovk(Scope::External),
output,
zip212_enforcement(&st.network(), height),
);
if result.is_some() {
return Ok(result.map(|(note, addr, memo)| {
(
Note::Sapling(note),
addr.into(),
MemoBytes::from_bytes(&memo).expect("correct length"),
)
}));
}
}
Ok(None)
}
fn received_note_count(
summary: &zcash_client_backend::data_api::chain::ScanSummary,
) -> usize {
summary.received_sapling_note_count()
}
}
pub(crate) fn test_prover() -> impl SpendProver + OutputProver {
LocalTxProver::bundled()
}
#[cfg(feature = "orchard")]
use zcash_client_backend::data_api::testing::orchard::OrchardPoolTester;
#[test]
fn send_single_step_proposed_transfer() {
@ -662,40 +499,30 @@ pub(crate) mod tests {
#[test]
#[cfg(feature = "orchard")]
fn pool_crossing_required() {
use crate::wallet::orchard::tests::OrchardPoolTester;
testing::pool::pool_crossing_required::<SaplingPoolTester, OrchardPoolTester>()
}
#[test]
#[cfg(feature = "orchard")]
fn fully_funded_fully_private() {
use crate::wallet::orchard::tests::OrchardPoolTester;
testing::pool::fully_funded_fully_private::<SaplingPoolTester, OrchardPoolTester>()
}
#[test]
#[cfg(all(feature = "orchard", feature = "transparent-inputs"))]
fn fully_funded_send_to_t() {
use crate::wallet::orchard::tests::OrchardPoolTester;
testing::pool::fully_funded_send_to_t::<SaplingPoolTester, OrchardPoolTester>()
}
#[test]
#[cfg(feature = "orchard")]
fn multi_pool_checkpoint() {
use crate::wallet::orchard::tests::OrchardPoolTester;
testing::pool::multi_pool_checkpoint::<SaplingPoolTester, OrchardPoolTester>()
}
#[test]
#[cfg(feature = "orchard")]
fn multi_pool_checkpoints_with_pruning() {
use crate::wallet::orchard::tests::OrchardPoolTester;
testing::pool::multi_pool_checkpoints_with_pruning::<SaplingPoolTester, OrchardPoolTester>()
}
}

View File

@ -587,6 +587,10 @@ pub(crate) mod tests {
use zcash_client_backend::data_api::{
chain::{ChainState, CommitmentTreeRoot},
scanning::{spanning_tree::testing::scan_range, ScanPriority},
testing::{
pool::ShieldedPoolTester, sapling::SaplingPoolTester, AddressType, FakeCompactOutput,
InitialChainState, TestBuilder, TestState,
},
AccountBirthday, Ratio, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT,
};
use zcash_primitives::{
@ -594,28 +598,28 @@ pub(crate) mod tests {
consensus::{BlockHeight, NetworkUpgrade, Parameters},
transaction::components::amount::NonNegativeAmount,
};
use zcash_protocol::local_consensus::LocalNetwork;
use crate::{
error::SqliteClientError,
testing::{
pool::ShieldedPoolTester, AddressType, BlockCache, FakeCompactOutput,
InitialChainState, TestBuilder, TestState,
},
wallet::{
sapling::tests::SaplingPoolTester,
scanning::{insert_queue_entries, replace_queue_entries, suggest_scan_ranges},
db::{TestDb, TestDbFactory},
BlockCache,
},
wallet::scanning::{insert_queue_entries, replace_queue_entries, suggest_scan_ranges},
VERIFY_LOOKAHEAD,
};
#[cfg(feature = "orchard")]
use {
crate::wallet::orchard::tests::OrchardPoolTester,
incrementalmerkletree::Level,
orchard::tree::MerkleHashOrchard,
std::{convert::Infallible, num::NonZeroU32},
zcash_client_backend::{
data_api::{wallet::input_selection::GreedyInputSelector, WalletCommitmentTrees},
data_api::{
testing::orchard::OrchardPoolTester, wallet::input_selection::GreedyInputSelector,
WalletCommitmentTrees,
},
fees::{standard, DustOutputPolicy},
wallet::OvkPolicy,
},
@ -646,7 +650,8 @@ pub(crate) mod tests {
let initial_height_offset = 310;
let mut st = TestBuilder::new()
.with_block_cache()
.with_data_store_factory(TestDbFactory)
.with_block_cache(BlockCache::new())
.with_initial_chain_state(|rng, network| {
let sapling_activation_height =
network.activation_height(NetworkUpgrade::Sapling).unwrap();
@ -728,7 +733,7 @@ pub(crate) mod tests {
// Verify the that adjacent range needed to make the note spendable has been prioritized.
let sap_active = u32::from(sapling_activation_height);
assert_matches!(
st.wallet().suggest_scan_ranges(),
suggest_scan_ranges(st.wallet().conn(), Historic),
Ok(scan_ranges) if scan_ranges == vec![
scan_range((sap_active + 300)..(sap_active + 310), FoundNote)
]
@ -736,7 +741,7 @@ pub(crate) mod tests {
// Check that the scanned range has been properly persisted.
assert_matches!(
suggest_scan_ranges(&st.wallet().conn, Scanned),
suggest_scan_ranges(st.wallet().conn(), Scanned),
Ok(scan_ranges) if scan_ranges == vec![
scan_range((sap_active + 300)..(sap_active + 310), FoundNote),
scan_range((sap_active + 310)..(sap_active + 320), Scanned)
@ -754,7 +759,7 @@ pub(crate) mod tests {
// Check the scan range again, we should see a `ChainTip` range for the period we've been
// offline.
assert_matches!(
st.wallet().suggest_scan_ranges(),
suggest_scan_ranges(st.wallet().conn(), Historic),
Ok(scan_ranges) if scan_ranges == vec![
scan_range((sap_active + 320)..(sap_active + 341), ChainTip),
scan_range((sap_active + 300)..(sap_active + 310), ChainTip)
@ -771,7 +776,7 @@ pub(crate) mod tests {
// Check the scan range again, we should see a `Validate` range for the previous wallet
// tip, and then a `ChainTip` for the remaining range.
assert_matches!(
st.wallet().suggest_scan_ranges(),
suggest_scan_ranges(st.wallet().conn(), Historic),
Ok(scan_ranges) if scan_ranges == vec![
scan_range((sap_active + 320)..(sap_active + 330), Verify),
scan_range((sap_active + 330)..(sap_active + 451), ChainTip),
@ -805,9 +810,15 @@ pub(crate) mod tests {
birthday_offset: u32,
prior_block_hash: BlockHash,
insert_prior_roots: bool,
) -> (TestState<BlockCache>, T::Fvk, AccountBirthday, u32) {
) -> (
TestState<BlockCache, TestDb, LocalNetwork>,
T::Fvk,
AccountBirthday,
u32,
) {
let st = TestBuilder::new()
.with_block_cache()
.with_data_store_factory(TestDbFactory)
.with_block_cache(BlockCache::new())
.with_initial_chain_state(|rng, network| {
// We set the Sapling and Orchard frontiers at the birthday height to be
// 1234 notes into the second shard.
@ -892,7 +903,7 @@ pub(crate) mod tests {
// The range up to the wallet's birthday height is ignored.
scan_range(sap_active..birthday_height, Ignored),
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
}
@ -900,7 +911,10 @@ pub(crate) mod tests {
fn update_chain_tip_before_create_account() {
use ScanPriority::*;
let mut st = TestBuilder::new().with_block_cache().build();
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.with_block_cache(BlockCache::new())
.build();
let sap_active = st.sapling_activation_height();
// Update the chain tip.
@ -912,7 +926,7 @@ pub(crate) mod tests {
// The range up to the chain end is ignored.
scan_range(sap_active.into()..chain_end, Ignored),
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
// Now add an account.
@ -933,7 +947,7 @@ pub(crate) mod tests {
// The range up to the wallet's birthday height is ignored.
scan_range(sap_active.into()..wallet_birthday.into(), Ignored),
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
}
@ -978,7 +992,7 @@ pub(crate) mod tests {
scan_range(sap_active..wallet_birthday, Ignored),
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
}
@ -1022,7 +1036,7 @@ pub(crate) mod tests {
scan_range(sap_active..birthday.height().into(), Ignored),
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
}
@ -1051,7 +1065,8 @@ pub(crate) mod tests {
// notes beyond the end of the first shard.
let frontier_tree_size: u32 = (0x1 << 16) + 1234;
let mut st = TestBuilder::new()
.with_block_cache()
.with_data_store_factory(TestDbFactory)
.with_block_cache(BlockCache::new())
.with_initial_chain_state(|rng, network| {
let birthday_height =
network.activation_height(NetworkUpgrade::Nu5).unwrap() + birthday_offset;
@ -1123,7 +1138,7 @@ pub(crate) mod tests {
),
pre_birthday_range.clone(),
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
// Simulate that in the blocks between the wallet birthday and the max_scanned height,
@ -1154,7 +1169,7 @@ pub(crate) mod tests {
pre_birthday_range.clone(),
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
// Now simulate shutting down, and then restarting 90 blocks later, after a shard
@ -1180,7 +1195,7 @@ pub(crate) mod tests {
.unwrap();
// Just inserting the subtree roots doesn't affect the scan ranges.
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
let new_tip = last_shard_start + 20;
@ -1213,7 +1228,7 @@ pub(crate) mod tests {
pre_birthday_range,
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
}
@ -1243,7 +1258,8 @@ pub(crate) mod tests {
// notes beyond the end of the first shard.
let frontier_tree_size: u32 = (0x1 << 16) + 1234;
let mut st = TestBuilder::new()
.with_block_cache()
.with_data_store_factory(TestDbFactory)
.with_block_cache(BlockCache::new())
.with_initial_chain_state(|rng, network| {
let birthday_height =
network.activation_height(NetworkUpgrade::Nu5).unwrap() + birthday_offset;
@ -1313,7 +1329,7 @@ pub(crate) mod tests {
scan_range(sap_active.into()..birthday.height().into(), Ignored),
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
// Simulate that in the blocks between the wallet birthday and the max_scanned height,
@ -1366,6 +1382,7 @@ pub(crate) mod tests {
{
let mut shard_stmt = st
.wallet_mut()
.db_mut()
.conn
.prepare("SELECT shard_index, subtree_end_height FROM sapling_tree_shards")
.unwrap();
@ -1381,6 +1398,7 @@ pub(crate) mod tests {
{
let mut shard_stmt = st
.wallet_mut()
.db_mut()
.conn
.prepare("SELECT shard_index, subtree_end_height FROM orchard_tree_shards")
.unwrap();
@ -1409,7 +1427,7 @@ pub(crate) mod tests {
scan_range(sap_active.into()..birthday.height().into(), Ignored),
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
// We've crossed a subtree boundary, but only in one pool. We still only have one scanned
@ -1427,7 +1445,9 @@ pub(crate) mod tests {
fn replace_queue_entries_merges_previous_range() {
use ScanPriority::*;
let mut st = TestBuilder::new().build();
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.build();
let ranges = vec![
scan_range(150..200, ChainTip),
@ -1436,16 +1456,16 @@ pub(crate) mod tests {
];
{
let tx = st.wallet_mut().conn.transaction().unwrap();
let tx = st.wallet_mut().conn_mut().transaction().unwrap();
insert_queue_entries(&tx, ranges.iter()).unwrap();
tx.commit().unwrap();
}
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, ranges);
{
let tx = st.wallet_mut().conn.transaction().unwrap();
let tx = st.wallet_mut().conn_mut().transaction().unwrap();
replace_queue_entries::<SqliteClientError>(
&tx,
&(BlockHeight::from(150)..BlockHeight::from(160)),
@ -1462,7 +1482,7 @@ pub(crate) mod tests {
scan_range(0..100, Ignored),
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
}
@ -1470,7 +1490,9 @@ pub(crate) mod tests {
fn replace_queue_entries_merges_subsequent_range() {
use ScanPriority::*;
let mut st = TestBuilder::new().build();
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.build();
let ranges = vec![
scan_range(150..200, ChainTip),
@ -1479,16 +1501,16 @@ pub(crate) mod tests {
];
{
let tx = st.wallet_mut().conn.transaction().unwrap();
let tx = st.wallet_mut().conn_mut().transaction().unwrap();
insert_queue_entries(&tx, ranges.iter()).unwrap();
tx.commit().unwrap();
}
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, ranges);
{
let tx = st.wallet_mut().conn.transaction().unwrap();
let tx = st.wallet_mut().conn_mut().transaction().unwrap();
replace_queue_entries::<SqliteClientError>(
&tx,
&(BlockHeight::from(90)..BlockHeight::from(100)),
@ -1505,7 +1527,7 @@ pub(crate) mod tests {
scan_range(0..90, Ignored),
];
let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), Ignored).unwrap();
assert_eq!(actual, expected);
}
@ -1534,14 +1556,15 @@ pub(crate) mod tests {
#[cfg(feature = "orchard")]
fn prepare_orchard_block_spanning_test(
with_birthday_subtree_root: bool,
) -> TestState<BlockCache> {
) -> TestState<BlockCache, TestDb, LocalNetwork> {
let birthday_nu5_offset = 5000;
let birthday_prior_block_hash = BlockHash([0; 32]);
// We set the Sapling and Orchard frontiers at the birthday block initial state to 50
// notes back from the end of the second shard.
let birthday_tree_size: u32 = (0x1 << 17) - 50;
let mut st = TestBuilder::new()
.with_block_cache()
.with_data_store_factory(TestDbFactory)
.with_block_cache(BlockCache::new())
.with_initial_chain_state(|rng, network| {
let birthday_height =
network.activation_height(NetworkUpgrade::Nu5).unwrap() + birthday_nu5_offset;
@ -1674,6 +1697,8 @@ pub(crate) mod tests {
#[test]
#[cfg(feature = "orchard")]
fn orchard_block_spanning_tip_boundary_complete() {
use zcash_client_backend::data_api::Account as _;
let mut st = prepare_orchard_block_spanning_test(true);
let account = st.test_account().cloned().unwrap();
let birthday = account.birthday();
@ -1701,27 +1726,24 @@ pub(crate) mod tests {
),
];
let actual = suggest_scan_ranges(&st.wallet().conn, ScanPriority::Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), ScanPriority::Ignored).unwrap();
assert_eq!(actual, expected);
// Scan the chain-tip range.
st.scan_cached_blocks(birthday.height() + 12, 112);
// We haven't yet discovered our note, so balances should still be zero
assert_eq!(
st.get_total_balance(account.account_id()),
NonNegativeAmount::ZERO
);
assert_eq!(st.get_total_balance(account.id()), NonNegativeAmount::ZERO);
// Now scan the historic range; this should discover our note, which should now be
// spendable.
st.scan_cached_blocks(birthday.height(), 12);
assert_eq!(
st.get_total_balance(account.account_id()),
st.get_total_balance(account.id()),
NonNegativeAmount::const_from_u64(100000)
);
assert_eq!(
st.get_spendable_balance(account.account_id(), 10),
st.get_spendable_balance(account.id(), 10),
NonNegativeAmount::const_from_u64(100000)
);
@ -1729,7 +1751,7 @@ pub(crate) mod tests {
let to_extsk = OrchardPoolTester::sk(&[0xf5; 32]);
let to = OrchardPoolTester::sk_default_address(&to_extsk);
let request = zip321::TransactionRequest::new(vec![zip321::Payment::without_memo(
to.to_zcash_address(&st.network()),
to.to_zcash_address(st.network()),
NonNegativeAmount::const_from_u64(10000),
)])
.unwrap();
@ -1747,7 +1769,7 @@ pub(crate) mod tests {
let proposal = st
.propose_transfer(
account.account_id(),
account.id(),
input_selector,
request,
NonZeroU32::new(10).unwrap(),
@ -1767,6 +1789,8 @@ pub(crate) mod tests {
#[test]
#[cfg(feature = "orchard")]
fn orchard_block_spanning_tip_boundary_incomplete() {
use zcash_client_backend::data_api::Account as _;
let mut st = prepare_orchard_block_spanning_test(false);
let account = st.test_account().cloned().unwrap();
let birthday = account.birthday();
@ -1790,27 +1814,24 @@ pub(crate) mod tests {
),
];
let actual = suggest_scan_ranges(&st.wallet().conn, ScanPriority::Ignored).unwrap();
let actual = suggest_scan_ranges(st.wallet().conn(), ScanPriority::Ignored).unwrap();
assert_eq!(actual, expected);
// Scan the chain-tip range, but omitting the spanning block.
st.scan_cached_blocks(birthday.height() + 13, 112);
// We haven't yet discovered our note, so balances should still be zero
assert_eq!(
st.get_total_balance(account.account_id()),
NonNegativeAmount::ZERO
);
assert_eq!(st.get_total_balance(account.id()), NonNegativeAmount::ZERO);
// Now scan the historic range; this should discover our note but not
// complete the tree. The note should not be considered spendable.
st.scan_cached_blocks(birthday.height(), 12);
assert_eq!(
st.get_total_balance(account.account_id()),
st.get_total_balance(account.id()),
NonNegativeAmount::const_from_u64(100000)
);
assert_eq!(
st.get_spendable_balance(account.account_id(), 10),
st.get_spendable_balance(account.id(), 10),
NonNegativeAmount::ZERO
);
@ -1818,7 +1839,7 @@ pub(crate) mod tests {
let to_extsk = OrchardPoolTester::sk(&[0xf5; 32]);
let to = OrchardPoolTester::sk_default_address(&to_extsk);
let request = zip321::TransactionRequest::new(vec![zip321::Payment::without_memo(
to.to_zcash_address(&st.network()),
to.to_zcash_address(st.network()),
NonNegativeAmount::const_from_u64(10000),
)])
.unwrap();
@ -1835,7 +1856,7 @@ pub(crate) mod tests {
&GreedyInputSelector::new(change_strategy, DustOutputPolicy::default());
let proposal = st.propose_transfer(
account.account_id(),
account.id(),
input_selector,
request.clone(),
NonZeroU32::new(10).unwrap(),
@ -1848,7 +1869,7 @@ pub(crate) mod tests {
// Verify that it's now possible to create the proposal
let proposal = st.propose_transfer(
account.account_id(),
account.id(),
input_selector,
request,
NonZeroU32::new(10).unwrap(),

View File

@ -822,11 +822,17 @@ pub(crate) fn queue_transparent_spend_detection<P: consensus::Parameters>(
#[cfg(test)]
mod tests {
use crate::testing::{AddressType, TestBuilder, TestState};
use crate::testing::{
db::{TestDb, TestDbFactory},
BlockCache,
};
use sapling::zip32::ExtendedSpendingKey;
use zcash_client_backend::{
data_api::{
wallet::input_selection::GreedyInputSelector, InputSource, WalletRead, WalletWrite,
testing::{AddressType, TestBuilder, TestState},
wallet::input_selection::GreedyInputSelector,
Account as _, InputSource, WalletRead, WalletWrite,
},
encoding::AddressCodec,
fees::{fixed, DustOutputPolicy},
@ -842,14 +848,13 @@ mod tests {
#[test]
fn put_received_transparent_utxo() {
use crate::testing::TestBuilder;
let mut st = TestBuilder::new()
.with_data_store_factory(TestDbFactory)
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let birthday = st.test_account().unwrap().birthday().height();
let account_id = st.test_account().unwrap().account_id();
let account_id = st.test_account().unwrap().id();
let uaddr = st
.wallet()
.get_current_address(account_id)
@ -933,10 +938,10 @@ mod tests {
// Artificially delete the address from the addresses table so that
// we can ensure the update fails if the join doesn't work.
st.wallet()
.conn
.conn()
.execute(
"DELETE FROM addresses WHERE cached_transparent_receiver_address = ?",
[Some(taddr.encode(&st.wallet().params))],
[Some(taddr.encode(st.network()))],
)
.unwrap();
@ -949,14 +954,15 @@ mod tests {
use zcash_client_backend::ShieldedProtocol;
let mut st = TestBuilder::new()
.with_block_cache()
.with_data_store_factory(TestDbFactory)
.with_block_cache(BlockCache::new())
.with_account_from_sapling_activation(BlockHash([0; 32]))
.build();
let account = st.test_account().cloned().unwrap();
let uaddr = st
.wallet()
.get_current_address(account.account_id())
.get_current_address(account.id())
.unwrap()
.unwrap();
let taddr = uaddr.transparent().unwrap();
@ -971,17 +977,14 @@ mod tests {
}
st.scan_cached_blocks(start_height, 10);
let check_balance = |st: &TestState<_>, min_confirmations: u32, expected| {
let check_balance = |st: &TestState<_, TestDb, _>, min_confirmations: u32, expected| {
// Check the wallet summary returns the expected transparent balance.
let summary = st
.wallet()
.get_wallet_summary(min_confirmations)
.unwrap()
.unwrap();
let balance = summary
.account_balances()
.get(&account.account_id())
.unwrap();
let balance = summary.account_balances().get(&account.id()).unwrap();
// TODO: in the future, we will distinguish between available and total
// balance according to `min_confirmations`
assert_eq!(balance.unshielded(), expected);
@ -990,7 +993,7 @@ mod tests {
let mempool_height = st.wallet().chain_height().unwrap().unwrap() + 1;
assert_eq!(
st.wallet()
.get_transparent_balances(account.account_id(), mempool_height)
.get_transparent_balances(account.id(), mempool_height)
.unwrap()
.get(taddr)
.cloned()