diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 2f361ca48..18ab2a8d6 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -1166,13 +1166,20 @@ 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, Self::Error>; +} +/// Read-only operations required for testing light wallet functions. +/// +/// These methods expose internal details or unstable interfaces, primarily to enable use +/// of the [`testing`] framework. They should not be used in production software. +#[cfg(any(test, feature = "test-dependencies"))] +#[delegatable_trait] +pub trait WalletTest: WalletRead { /// 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>, Self::Error> { @@ -1181,7 +1188,6 @@ pub trait WalletRead { /// 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, diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs index a32e48f38..060f77108 100644 --- a/zcash_client_backend/src/data_api/testing.rs +++ b/zcash_client_backend/src/data_api/testing.rs @@ -56,6 +56,7 @@ use crate::{ ShieldedProtocol, }; +use super::WalletTest; #[allow(deprecated)] use super::{ chain::{scan_cached_blocks, BlockSource, ChainState, CommitmentTreeRoot, ScanSummary}, @@ -88,6 +89,7 @@ pub mod orchard; pub mod pool; pub mod sapling; +/// Information about a transaction that the wallet is interested in. pub struct TransactionSummary { account_id: AccountId, txid: TxId, @@ -105,8 +107,12 @@ pub struct TransactionSummary { } impl TransactionSummary { + /// Constructs a `TransactionSummary` from its parts. + /// + /// See the documentation for each getter method below to determine how each method + /// argument should be prepared. #[allow(clippy::too_many_arguments)] - pub fn new( + pub fn from_parts( account_id: AccountId, txid: TxId, expiry_height: Option, @@ -138,59 +144,97 @@ impl TransactionSummary { } } + /// Returns the wallet-internal ID for the account that this transaction was received + /// by or sent from. pub fn account_id(&self) -> &AccountId { &self.account_id } + /// Returns the transaction's ID. pub fn txid(&self) -> TxId { self.txid } + /// Returns the expiry height of the transaction, if known. + /// + /// - `None` means that the expiry height is unknown. + /// - `Some(0)` means that the transaction does not expire. pub fn expiry_height(&self) -> Option { self.expiry_height } + /// Returns the height of the mined block containing this transaction, or `None` if + /// the wallet has not yet observed the transaction to be mined. pub fn mined_height(&self) -> Option { self.mined_height } + /// Returns the net change in balance that this transaction caused to the account. + /// + /// For example, an account-internal transaction (such as a shielding operation) would + /// show `-fee_paid` as the account value delta. pub fn account_value_delta(&self) -> ZatBalance { self.account_value_delta } + /// Returns the fee paid by this transaction, if known. pub fn fee_paid(&self) -> Option { self.fee_paid } + /// Returns the number of notes spent by the account in this transaction. pub fn spent_note_count(&self) -> usize { self.spent_note_count } + /// Returns `true` if the account received a change note as part of this transaction. + /// + /// This implies that the transaction was (at least in part) sent from the account. pub fn has_change(&self) -> bool { self.has_change } + /// Returns the number of notes created in this transaction that were sent to a + /// wallet-external address. pub fn sent_note_count(&self) -> usize { self.sent_note_count } + /// Returns the number of notes created in this transaction that were received by the + /// account. pub fn received_note_count(&self) -> usize { self.received_note_count } + /// Returns `true` if, from the wallet's current view of the chain, this transaction + /// expired before it was mined. pub fn expired_unmined(&self) -> bool { self.expired_unmined } + /// Returns the number of non-empty memos viewable by the account in this transaction. pub fn memo_count(&self) -> usize { self.memo_count } + /// Returns `true` if this is detectably a shielding transaction. + /// + /// Specifically, `true` means that at a minimum: + /// - All of the wallet-spent and wallet-received notes are consistent with a + /// shielding transaction. + /// - The transaction contains at least one wallet-spent output. + /// - The transaction contains at least one wallet-received note. + /// - We do not know about any external outputs of the transaction. + /// + /// There may be some shielding transactions for which this method returns `false`, + /// due to them not being detectable by the wallet as shielding transactions under the + /// above metrics. pub fn is_shielding(&self) -> bool { self.is_shielding } } +/// Metadata about a block generated by [`TestState`]. #[derive(Clone, Debug)] pub struct CachedBlock { chain_state: ChainState, @@ -199,14 +243,20 @@ pub struct CachedBlock { } impl CachedBlock { - pub fn none(sapling_activation_height: BlockHeight) -> Self { + /// Produces metadata for a block "before shielded time", when the Sapling and Orchard + /// trees were (by definition) empty. + /// + /// `block_height` must be a height before Sapling activation (and therefore also + /// before NU5 activation). + pub fn none(block_height: BlockHeight) -> Self { Self { - chain_state: ChainState::empty(sapling_activation_height, BlockHash([0; 32])), + chain_state: ChainState::empty(block_height, BlockHash([0; 32])), sapling_end_size: 0, orchard_end_size: 0, } } + /// Produces metadata for a block as of the given chain state. pub fn at(chain_state: ChainState, sapling_end_size: u32, orchard_end_size: u32) -> Self { assert_eq!( chain_state.final_sapling_tree().tree_size() as u32, @@ -265,19 +315,27 @@ impl CachedBlock { } } + /// Returns the height of this block. pub fn height(&self) -> BlockHeight { self.chain_state.block_height() } + /// Returns the size of the Sapling note commitment tree as of the end of this block. pub fn sapling_end_size(&self) -> u32 { self.sapling_end_size } + /// Returns the size of the Orchard note commitment tree as of the end of this block. pub fn orchard_end_size(&self) -> u32 { self.orchard_end_size } } +/// The test account configured for a [`TestState`]. +/// +/// Create this by calling either [`TestBuilder::with_account_from_sapling_activation`] or +/// [`TestBuilder::with_account_having_current_birthday`] while setting up a test, and +/// then access it with [`TestState::test_account`]. #[derive(Clone)] pub struct TestAccount { account: A, @@ -286,14 +344,17 @@ pub struct TestAccount { } impl TestAccount { + /// Returns the underlying wallet account. pub fn account(&self) -> &A { &self.account } + /// Returns the account's unified spending key. pub fn usk(&self) -> &UnifiedSpendingKey { &self.usk } + /// Returns the birthday that was configured for the account. pub fn birthday(&self) -> &AccountBirthday { &self.birthday } @@ -319,14 +380,23 @@ impl Account for TestAccount { } } -pub trait Reset: WalletRead + Sized { +/// Trait method exposing the ability to reset the wallet within a test. +// TODO: Does this need to exist separately from DataStoreFactory? +pub trait Reset: WalletTest + Sized { + /// A handle that confers ownership of a specific wallet instance. type Handle; + /// Replaces the wallet in `st` (via [`TestState::wallet_mut`]) with a new wallet + /// database. + /// + /// This does not recreate accounts. The resulting wallet in `st` has no test account. + /// + /// Returns the old wallet. fn reset(st: &mut TestState) -> Self::Handle; } -/// The state for a `zcash_client_sqlite` test. -pub struct TestState { +/// The state for a `zcash_client_backend` test. +pub struct TestState { cache: Cache, cached_blocks: BTreeMap, latest_block_height: Option, @@ -336,7 +406,7 @@ pub struct TestState { rng: ChaChaRng, } -impl TestState { +impl TestState { /// Exposes an immutable reference to the test's `DataStore`. pub fn wallet(&self) -> &DataStore { &self.wallet_data @@ -358,7 +428,7 @@ impl TestState } } -impl +impl TestState { /// Convenience method for obtaining the Sapling activation height for the network under test. @@ -405,7 +475,7 @@ impl impl TestState where Network: consensus::Parameters, - DataStore: WalletWrite, + DataStore: WalletTest + WalletWrite, ::Error: fmt::Debug, { /// Exposes an immutable reference to the test's [`BlockSource`]. @@ -414,6 +484,8 @@ where self.cache.block_source() } + /// Returns the cached chain state corresponding to the latest block generated by this + /// `TestState`. pub fn latest_cached_block(&self) -> Option<&CachedBlock> { self.latest_block_height .as_ref() @@ -697,7 +769,7 @@ where Cache: TestCache, ::Error: fmt::Debug, ParamsT: consensus::Parameters + Send + 'static, - DbT: InputSource + WalletWrite + WalletCommitmentTrees, + DbT: InputSource + WalletTest + WalletWrite + WalletCommitmentTrees, ::AccountId: ConditionallySelectable + Default + Send + 'static, { /// Invokes [`scan_cached_blocks`] with the given arguments, expecting success. @@ -760,6 +832,7 @@ where AccountIdT: std::cmp::Eq + std::hash::Hash, ErrT: std::fmt::Debug, DbT: InputSource + + WalletTest + WalletWrite + WalletCommitmentTrees, ::AccountId: ConditionallySelectable + Default + Send + 'static, @@ -1030,10 +1103,13 @@ where f(binding.account_balances().get(&account).unwrap()) } + /// Returns the total balance in the given account at this point in the test. pub fn get_total_balance(&self, account: AccountIdT) -> NonNegativeAmount { self.with_account_balance(account, 0, |balance| balance.total()) } + /// Returns the balance in the given account that is spendable with the given number + /// of confirmations at this point in the test. pub fn get_spendable_balance( &self, account: AccountIdT, @@ -1044,6 +1120,8 @@ where }) } + /// Returns the balance in the given account that is detected but not yet spendable + /// with the given number of confirmations at this point in the test. pub fn get_pending_shielded_balance( &self, account: AccountIdT, @@ -1055,6 +1133,8 @@ where .unwrap() } + /// Returns the amount of change in the given account that is not yet spendable with + /// the given number of confirmations at this point in the test. #[allow(dead_code)] pub fn get_pending_change( &self, @@ -1066,10 +1146,23 @@ where }) } + /// Returns a summary of the wallet at this point in the test. pub fn get_wallet_summary(&self, min_confirmations: u32) -> Option> { self.wallet().get_wallet_summary(min_confirmations).unwrap() } +} +impl TestState +where + ParamsT: consensus::Parameters + Send + 'static, + AccountIdT: std::cmp::Eq + std::hash::Hash, + ErrT: std::fmt::Debug, + DbT: InputSource + + WalletTest + + WalletWrite + + WalletCommitmentTrees, + ::AccountId: ConditionallySelectable + Default + Send + 'static, +{ /// Returns a transaction from the history. #[allow(dead_code)] pub fn get_tx_from_history( @@ -1113,6 +1206,8 @@ impl TestState { // } } +/// Helper method for constructing a [`GreedyInputSelector`] with a +/// [`standard::SingleOutputChangeStrategy`]. pub fn input_selector( fee_rule: StandardFeeRule, change_memo: Option<&str>, @@ -1135,13 +1230,22 @@ fn check_proposal_serialization_roundtrip( assert_matches!(deserialized_proposal, Ok(r) if &r == proposal); } +/// The initial chain state for a test. +/// +/// This is returned from the closure passed to [`TestBuilder::with_initial_chain_state`] +/// to configure the test state with a starting chain position, to which subsequent test +/// activity is applied. pub struct InitialChainState { + /// Information about the chain's state as of the chain tip. pub chain_state: ChainState, + /// Roots of the completed Sapling subtrees as of this chain state. pub prior_sapling_roots: Vec>, + /// Roots of the completed Orchard subtrees as of this chain state. #[cfg(feature = "orchard")] pub prior_orchard_roots: Vec>, } +/// Trait representing the ability to construct a new data store for use in a test. pub trait DataStoreFactory { type Error: core::fmt::Debug; type AccountId: ConditionallySelectable + Default + Hash + Eq + Send + 'static; @@ -1149,13 +1253,15 @@ pub trait DataStoreFactory { type DsError: core::fmt::Debug; type DataStore: InputSource + WalletRead + + WalletTest + WalletWrite + WalletCommitmentTrees; + /// Constructs a new data store. fn new_data_store(&self, network: LocalNetwork) -> Result; } -/// A builder for a `zcash_client_sqlite` test. +/// A [`TestState`] builder, that configures the environment for a test. pub struct TestBuilder { rng: ChaChaRng, network: LocalNetwork, @@ -1167,6 +1273,10 @@ pub struct TestBuilder { } impl TestBuilder<(), ()> { + /// The default network used by [`TestBuilder::new`]. + /// + /// This is a fake network where Sapling through NU5 activate at the same height. We + /// pick height 100,000 to be large enough to handle any hard-coded test offsets. pub const DEFAULT_NETWORK: LocalNetwork = LocalNetwork { overwinter: Some(BlockHeight::from_u32(1)), sapling: Some(BlockHeight::from_u32(100_000)), @@ -1183,8 +1293,6 @@ impl TestBuilder<(), ()> { pub fn new() -> Self { TestBuilder { rng: ChaChaRng::seed_from_u64(0), - // Use a fake network where Sapling through NU5 activate at the same height. - // We pick 100,000 to be large enough to handle any hard-coded test offsets. network: Self::DEFAULT_NETWORK, cache: (), ds_factory: (), @@ -1217,6 +1325,7 @@ impl TestBuilder<(), A> { } impl TestBuilder { + /// Adds a wallet data store to the test environment. pub fn with_data_store_factory( self, ds_factory: DsFactory, @@ -1234,6 +1343,88 @@ impl TestBuilder { } impl TestBuilder { + /// Configures the test to start with the given initial chain state. + /// + /// # Panics + /// + /// - Must not be called twice. + /// - Must be called before [`Self::with_account_from_sapling_activation`] or + /// [`Self::with_account_having_current_birthday`]. + /// + /// # Examples + /// + /// ``` + /// use std::num::NonZeroU8; + /// + /// use incrementalmerkletree::frontier::Frontier; + /// use zcash_primitives::{block::BlockHash, consensus::Parameters}; + /// use zcash_protocol::consensus::NetworkUpgrade; + /// use zcash_client_backend::data_api::{ + /// chain::{ChainState, CommitmentTreeRoot}, + /// testing::{InitialChainState, TestBuilder}, + /// }; + /// + /// // For this test, we'll start inserting leaf notes 5 notes after the end of the + /// // third subtree, with a gap of 10 blocks. After `scan_cached_blocks`, the scan + /// // queue should have a requested scan range of 300..310 with `FoundNote` priority, + /// // 310..320 with `Scanned` priority. We set both Sapling and Orchard to the same + /// // initial tree size for simplicity. + /// let prior_block_hash = BlockHash([0; 32]); + /// let initial_sapling_tree_size: u32 = (0x1 << 16) * 3 + 5; + /// let initial_orchard_tree_size: u32 = (0x1 << 16) * 3 + 5; + /// let initial_height_offset = 310; + /// + /// let mut st = TestBuilder::new() + /// .with_initial_chain_state(|rng, network| { + /// // For simplicity, assume Sapling and NU5 activated at the same height. + /// let sapling_activation_height = + /// network.activation_height(NetworkUpgrade::Sapling).unwrap(); + /// + /// // Construct a fake chain state for the end of block 300 + /// let (prior_sapling_roots, sapling_initial_tree) = + /// Frontier::random_with_prior_subtree_roots( + /// rng, + /// initial_sapling_tree_size.into(), + /// NonZeroU8::new(16).unwrap(), + /// ); + /// let prior_sapling_roots = prior_sapling_roots + /// .into_iter() + /// .zip(1u32..) + /// .map(|(root, i)| { + /// CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root) + /// }) + /// .collect::>(); + /// + /// #[cfg(feature = "orchard")] + /// let (prior_orchard_roots, orchard_initial_tree) = + /// Frontier::random_with_prior_subtree_roots( + /// rng, + /// initial_orchard_tree_size.into(), + /// NonZeroU8::new(16).unwrap(), + /// ); + /// #[cfg(feature = "orchard")] + /// let prior_orchard_roots = prior_orchard_roots + /// .into_iter() + /// .zip(1u32..) + /// .map(|(root, i)| { + /// CommitmentTreeRoot::from_parts(sapling_activation_height + (100 * i), root) + /// }) + /// .collect::>(); + /// + /// InitialChainState { + /// chain_state: ChainState::new( + /// sapling_activation_height + initial_height_offset - 1, + /// prior_block_hash, + /// sapling_initial_tree, + /// #[cfg(feature = "orchard")] + /// orchard_initial_tree, + /// ), + /// prior_sapling_roots, + /// #[cfg(feature = "orchard")] + /// prior_orchard_roots, + /// } + /// }); + /// ``` pub fn with_initial_chain_state( mut self, chain_state: impl FnOnce(&mut ChaChaRng, &LocalNetwork) -> InitialChainState, @@ -1244,6 +1435,13 @@ impl TestBuilder { self } + /// Configures the environment with a [`TestAccount`] that has a birthday at Sapling + /// activation. + /// + /// # Panics + /// + /// - Must not be called twice. + /// - Do not call both [`Self::with_account_having_current_birthday`] and this method. pub fn with_account_from_sapling_activation(mut self, prev_hash: BlockHash) -> Self { assert!(self.account_birthday.is_none()); self.account_birthday = Some(AccountBirthday::from_parts( @@ -1259,6 +1457,14 @@ impl TestBuilder { self } + /// Configures the environment with a [`TestAccount`] that has a birthday one block + /// after the initial chain state. + /// + /// # Panics + /// + /// - Must not be called twice. + /// - Must call [`Self::with_initial_chain_state`] before calling this method. + /// - Do not call both [`Self::with_account_from_sapling_activation`] and this method. pub fn with_account_having_current_birthday(mut self) -> Self { assert!(self.account_birthday.is_none()); assert!(self.initial_chain_state.is_some()); @@ -1275,8 +1481,12 @@ impl TestBuilder { /// Sets the account index for the test account. /// - /// Call either [`Self::with_account_from_sapling_activation`] or - /// [`Self::with_account_having_current_birthday`] before calling this method. + /// Does nothing unless either [`Self::with_account_from_sapling_activation`] or + /// [`Self::with_account_having_current_birthday`] is also called. + /// + /// # Panics + /// + /// - Must not be called twice. pub fn set_account_index(mut self, index: zip32::AccountId) -> Self { assert!(self.account_index.is_none()); self.account_index = Some(index); @@ -1381,13 +1591,21 @@ impl TestBuilder { /// Trait used by tests that require a full viewing key. pub trait TestFvk { + /// The type of nullifier corresponding to the kind of note that this full viewing key + /// can detect (and that its corresponding spending key can spend). type Nullifier: Copy; + /// Returns the Sapling outgoing viewing key corresponding to this full viewing key, + /// if any. fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey>; + /// Returns the Orchard outgoing viewing key corresponding to this full viewing key, + /// if any. #[cfg(feature = "orchard")] fn orchard_ovk(&self, scope: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey>; + /// Adds a single spend to the given [`CompactTx`] of a note previously received by + /// this full viewing key. fn add_spend( &self, ctx: &mut CompactTx, @@ -1395,6 +1613,10 @@ pub trait TestFvk { rng: &mut R, ); + /// Adds a single output to the given [`CompactTx`] that will be received by this full + /// viewing key. + /// + /// `req` allows configuring how the full viewing key will detect the output. #[allow(clippy::too_many_arguments)] fn add_output( &self, @@ -1409,6 +1631,13 @@ pub trait TestFvk { rng: &mut R, ) -> Self::Nullifier; + /// Adds both a spend and an output to the given [`CompactTx`]. + /// + /// - If this is a Sapling full viewing key, the transaction will gain both a Spend + /// and an Output. + /// - If this is an Orchard full viewing key, the transaction will gain an Action. + /// + /// `req` allows configuring how the full viewing key will detect the output. #[allow(clippy::too_many_arguments)] fn add_logical_action( &self, @@ -1671,11 +1900,21 @@ impl TestFvk for ::orchard::keys::FullViewingKey { } } +/// Configures how a [`TestFvk`] receives a particular output. +/// +/// Used with [`TestFvk::add_output`] and [`TestFvk::add_logical_action`]. #[derive(Clone, Copy)] pub enum AddressType { + /// The output will be sent to the default address of the full viewing key. DefaultExternal, + /// The output will be sent to the specified diversified address of the full viewing + /// key. #[allow(dead_code)] DiversifiedExternal(DiversifierIndex), + /// The output will be sent to the internal receiver of the full viewing key. + /// + /// Such outputs are treated as "wallet-internal". A "recipient address" is **NEVER** + /// exposed to users. Internal, } @@ -1753,6 +1992,11 @@ fn fake_compact_tx(rng: &mut R) -> CompactTx { ctx } +/// A fake output of a [`CompactTx`]. +/// +/// Used with the following block generators: +/// - [`TestState::generate_next_block_multi`] +/// - [`TestState::generate_block_at`] #[derive(Clone)] pub struct FakeCompactOutput { fvk: Fvk, @@ -1761,6 +2005,7 @@ pub struct FakeCompactOutput { } impl FakeCompactOutput { + /// Constructs a new fake output with the given properties. pub fn new(fvk: Fvk, address_type: AddressType, value: NonNegativeAmount) -> Self { Self { fvk, @@ -1998,6 +2243,9 @@ pub trait TestCache { fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult; } +/// A convenience type for the note commitments contained within a [`CompactBlock`]. +/// +/// Indended for use as (part of) the [`TestCache::InsertResult`] associated type. pub struct NoteCommitments { sapling: Vec<::sapling::Node>, #[cfg(feature = "orchard")] @@ -2005,6 +2253,7 @@ pub struct NoteCommitments { } impl NoteCommitments { + /// Extracts the note commitments from the given compact block. pub fn from_compact_block(cb: &CompactBlock) -> Self { NoteCommitments { sapling: cb @@ -2029,17 +2278,20 @@ impl NoteCommitments { } } + /// Returns the Sapling note commitments. #[allow(dead_code)] pub fn sapling(&self) -> &[::sapling::Node] { self.sapling.as_ref() } + /// Returns the Orchard note commitments. #[cfg(feature = "orchard")] pub fn orchard(&self) -> &[MerkleHashOrchard] { self.orchard.as_ref() } } +/// A mock wallet data source that implements the bare minimum necessary to function. pub struct MockWalletDb { pub network: Network, pub sapling_tree: ShardTree< @@ -2056,6 +2308,7 @@ pub struct MockWalletDb { } impl MockWalletDb { + /// Constructs a new mock wallet data source. pub fn new(network: Network) -> Self { Self { network, diff --git a/zcash_client_backend/src/data_api/testing/orchard.rs b/zcash_client_backend/src/data_api/testing/orchard.rs index 6c8ef143d..d076a6d7e 100644 --- a/zcash_client_backend/src/data_api/testing/orchard.rs +++ b/zcash_client_backend/src/data_api/testing/orchard.rs @@ -25,11 +25,12 @@ use crate::{ data_api::{ chain::{CommitmentTreeRoot, ScanSummary}, testing::{pool::ShieldedPoolTester, TestState}, - DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, WalletSummary, + DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletSummary, WalletTest, }, wallet::{Note, ReceivedNote}, }; +/// Type for running pool-agnostic tests on the Orchard pool. pub struct OrchardPoolTester; impl ShieldedPoolTester for OrchardPoolTester { const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Orchard; @@ -40,7 +41,7 @@ impl ShieldedPoolTester for OrchardPoolTester { type MerkleTreeHash = MerkleHashOrchard; type Note = orchard::note::Note; - fn test_account_fvk( + fn test_account_fvk( st: &TestState, ) -> Self::Fvk { st.test_account_orchard().unwrap().clone() @@ -90,7 +91,7 @@ impl ShieldedPoolTester for OrchardPoolTester { MerkleHashOrchard::empty_root(level) } - fn put_subtree_roots( + fn put_subtree_roots( st: &mut TestState, start_index: u64, roots: &[CommitmentTreeRoot], @@ -103,7 +104,7 @@ impl ShieldedPoolTester for OrchardPoolTester { s.next_orchard_subtree_index() } - fn select_spendable_notes( + fn select_spendable_notes( st: &TestState, account: ::AccountId, target_value: Zatoshis, diff --git a/zcash_client_backend/src/data_api/testing/pool.rs b/zcash_client_backend/src/data_api/testing/pool.rs index fb7b83805..2bbfb11d5 100644 --- a/zcash_client_backend/src/data_api/testing/pool.rs +++ b/zcash_client_backend/src/data_api/testing/pool.rs @@ -23,7 +23,7 @@ use crate::{ testing::{AddressType, TestBuilder}, wallet::{decrypt_and_store_transaction, input_selection::GreedyInputSelector}, Account as _, DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, - WalletSummary, + WalletSummary, WalletTest, }, decrypt_transaction, fees::{standard, DustOutputPolicy}, @@ -34,6 +34,19 @@ 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. +/// +/// You should not need to implement this yourself; instead use [`SaplingPoolTester`] or +/// [`OrchardPoolTester`] as appropriate. +/// +/// [`SaplingPoolTester`]: super::sapling::SaplingPoolTester +#[cfg_attr( + feature = "orchard", + doc = "[`OrchardPoolTester`]: super::orchard::OrchardPoolTester" +)] +#[cfg_attr( + not(feature = "orchard"), + doc = "[`OrchardPoolTester`]: https://github.com/zcash/librustzcash/blob/0777cbc2def6ba6b99f96333eaf96c314c1f3a37/zcash_client_backend/src/data_api/testing/orchard.rs#L33" +)] pub trait ShieldedPoolTester { const SHIELDED_PROTOCOL: ShieldedProtocol; @@ -42,7 +55,7 @@ pub trait ShieldedPoolTester { type MerkleTreeHash; type Note; - fn test_account_fvk( + fn test_account_fvk( st: &TestState, ) -> Self::Fvk; fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk; @@ -68,7 +81,7 @@ pub trait ShieldedPoolTester { fn empty_tree_leaf() -> Self::MerkleTreeHash; fn empty_tree_root(level: Level) -> Self::MerkleTreeHash; - fn put_subtree_roots( + fn put_subtree_roots( st: &mut TestState, start_index: u64, roots: &[CommitmentTreeRoot], @@ -77,7 +90,7 @@ pub trait ShieldedPoolTester { fn next_subtree_index(s: &WalletSummary) -> u64; #[allow(clippy::type_complexity)] - fn select_spendable_notes( + fn select_spendable_notes( st: &TestState, account: ::AccountId, target_value: Zatoshis, @@ -99,6 +112,16 @@ pub trait ShieldedPoolTester { fn received_note_count(summary: &ScanSummary) -> usize; } +/// Tests sending funds within the given shielded pool in a single transaction. +/// +/// The test: +/// - Adds funds to the wallet in a single note. +/// - Checks that the wallet balances are correct. +/// - Constructs a request to spend part of that balance to an external address in the +/// same pool. +/// - Builds the transaction. +/// - Checks that the transaction was stored, and that the outputs are decryptable and +/// have the expected details. pub fn send_single_step_proposed_transfer( dsf: impl DataStoreFactory, cache: impl TestCache, diff --git a/zcash_client_backend/src/data_api/testing/sapling.rs b/zcash_client_backend/src/data_api/testing/sapling.rs index 6f9a78fb4..fe082c224 100644 --- a/zcash_client_backend/src/data_api/testing/sapling.rs +++ b/zcash_client_backend/src/data_api/testing/sapling.rs @@ -19,13 +19,14 @@ use zip32::Scope; use crate::{ data_api::{ chain::{CommitmentTreeRoot, ScanSummary}, - DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, WalletSummary, + DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletSummary, WalletTest, }, wallet::{Note, ReceivedNote}, }; use super::{pool::ShieldedPoolTester, TestState}; +/// Type for running pool-agnostic tests on the Sapling pool. pub struct SaplingPoolTester; impl ShieldedPoolTester for SaplingPoolTester { const SHIELDED_PROTOCOL: ShieldedProtocol = ShieldedProtocol::Sapling; @@ -36,7 +37,7 @@ impl ShieldedPoolTester for SaplingPoolTester { type MerkleTreeHash = sapling::Node; type Note = sapling::Note; - fn test_account_fvk( + fn test_account_fvk( st: &TestState, ) -> Self::Fvk { st.test_account_sapling().unwrap().clone() @@ -74,7 +75,7 @@ impl ShieldedPoolTester for SaplingPoolTester { ::sapling::Node::empty_root(level) } - fn put_subtree_roots( + fn put_subtree_roots( st: &mut TestState, start_index: u64, roots: &[CommitmentTreeRoot], @@ -87,7 +88,7 @@ impl ShieldedPoolTester for SaplingPoolTester { s.next_sapling_subtree_index() } - fn select_spendable_notes( + fn select_spendable_notes( st: &TestState, account: ::AccountId, target_value: Zatoshis, diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index fecf44229..57415820c 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -53,7 +53,7 @@ use zcash_client_backend::{ Account, AccountBirthday, AccountPurpose, AccountSource, BlockMetadata, DecryptedTransaction, InputSource, NullifierQuery, ScannedBlock, SeedRelevance, SentTransaction, SpendableNotes, TransactionDataRequest, WalletCommitmentTrees, WalletRead, - WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT, + WalletSummary, WalletTest, WalletWrite, SAPLING_SHARD_HEIGHT, }, keys::{ AddressGenerationError, UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey, @@ -614,13 +614,14 @@ impl, P: consensus::Parameters> WalletRead for W Ok(iter.collect()) } +} - #[cfg(any(test, feature = "test-dependencies"))] +#[cfg(any(test, feature = "test-dependencies"))] +impl, P: consensus::Parameters> WalletTest for WalletDb { fn get_tx_history(&self) -> Result>, Self::Error> { wallet::testing::get_tx_history(self.conn.borrow()) } - #[cfg(any(test, feature = "test-dependencies"))] fn get_sent_note_ids( &self, txid: &TxId, @@ -1725,7 +1726,8 @@ mod tests { use zcash_client_backend::data_api::{ chain::ChainState, testing::{TestBuilder, TestState}, - Account, AccountBirthday, AccountPurpose, AccountSource, WalletRead, WalletWrite, + Account, AccountBirthday, AccountPurpose, AccountSource, WalletRead, WalletTest, + WalletWrite, }; use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey}; use zcash_primitives::block::BlockHash; @@ -1837,7 +1839,7 @@ mod tests { AccountSource::Derived { seed_fingerprint: _, account_index } if account_index == zip32_index_2); } - fn check_collisions( + fn check_collisions( st: &mut TestState, ufvk: &UnifiedFullViewingKey, birthday: &AccountBirthday, diff --git a/zcash_client_sqlite/src/testing/db.rs b/zcash_client_sqlite/src/testing/db.rs index e980aa6ce..f972fdfde 100644 --- a/zcash_client_sqlite/src/testing/db.rs +++ b/zcash_client_sqlite/src/testing/db.rs @@ -43,6 +43,7 @@ use { #[derive(Delegate)] #[delegate(InputSource, target = "wallet_db")] #[delegate(WalletRead, target = "wallet_db")] +#[delegate(WalletTest, target = "wallet_db")] #[delegate(WalletWrite, target = "wallet_db")] #[delegate(WalletCommitmentTrees, target = "wallet_db")] pub(crate) struct TestDb { diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index 795d49f03..aff260a28 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -100,7 +100,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { use rand_core::OsRng; use zcash_client_backend::{ - data_api::{TransactionDataRequest, TransactionStatus}, + data_api::{TransactionDataRequest, TransactionStatus, WalletTest}, fees::ChangeValue, wallet::TransparentAddressMetadata, }; diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 63b275017..dc0dbef21 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -3194,7 +3194,7 @@ pub mod testing { let results = stmt .query_and_then::, SqliteClientError, _, _>([], |row| { - Ok(TransactionSummary::new( + Ok(TransactionSummary::from_parts( AccountId(row.get("account_id")?), TxId::from_bytes(row.get("txid")?), row.get::<_, Option>("expiry_height")?