From 026f5f6de67f49350ed9fef13ce7f49d3fa50154 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 4 Sep 2024 15:38:41 -0600 Subject: [PATCH 01/20] zcash_client_backend: Fix broken --all-features build. --- zcash_client_backend/Cargo.toml | 8 ++++++++ zcash_client_backend/src/tor.rs | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index fdc99cd12e..caecf5041f 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -214,6 +214,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 diff --git a/zcash_client_backend/src/tor.rs b/zcash_client_backend/src/tor.rs index a683c5be6b..ee1a636f73 100644 --- a/zcash_client_backend/src/tor.rs +++ b/zcash_client_backend/src/tor.rs @@ -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; From 69953ccd885f0faab89daff0f5afba608e58f314 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Wed, 4 Sep 2024 15:39:29 -0600 Subject: [PATCH 02/20] zcash_client_backend: Make `AccountId` an associated type of the `Account` trait. --- zcash_client_backend/CHANGELOG.md | 5 +++++ zcash_client_backend/src/data_api.rs | 16 +++++++++++----- zcash_client_sqlite/src/wallet.rs | 4 +++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 4aa903de03..3b44dce285 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -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 diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 17749f657d..302380d42c 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -346,9 +346,11 @@ pub enum AccountSource { } /// A set of capabilities that a client account must provide. -pub trait Account { +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 +379,9 @@ pub trait Account { } #[cfg(any(test, feature = "test-dependencies"))] -impl Account for (A, UnifiedFullViewingKey) { +impl Account for (A, UnifiedFullViewingKey) { + type AccountId = A; + fn id(&self) -> A { self.0 } @@ -398,7 +402,9 @@ impl Account for (A, UnifiedFullViewingKey) { } #[cfg(any(test, feature = "test-dependencies"))] -impl Account for (A, UnifiedIncomingViewingKey) { +impl Account for (A, UnifiedIncomingViewingKey) { + type AccountId = A; + fn id(&self) -> A { self.0 } @@ -804,7 +810,7 @@ pub trait WalletRead { type AccountId: Copy + Debug + Eq + Hash; /// The concrete account type used by this wallet backend. - type Account: Account; + type Account: Account; /// Returns a vector with the IDs of all accounts known to this wallet. fn get_account_ids(&self) -> Result, Self::Error>; diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index fa6ed78158..a54daa3dff 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -220,7 +220,9 @@ impl Account { } } -impl zcash_client_backend::data_api::Account for Account { +impl zcash_client_backend::data_api::Account for Account { + type AccountId = AccountId; + fn id(&self) -> AccountId { self.account_id } From 0ae5ac1a99425f65c2abbbc730416d11db83ba8b Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 5 Sep 2024 14:03:49 -0600 Subject: [PATCH 03/20] zcash_client_backend: Make `data_api` traits delegatable via `ambassador` for testing. --- Cargo.lock | 14 ++++++++++++++ Cargo.toml | 1 + zcash_client_backend/Cargo.toml | 2 ++ zcash_client_backend/src/data_api.rs | 19 +++++++++++++++---- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cdc8fc7a23..b932c2fd63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", @@ -5879,6 +5892,7 @@ dependencies = [ name = "zcash_client_sqlite" version = "0.11.2" dependencies = [ + "ambassador", "assert_matches", "bip32", "bls12_381", diff --git a/Cargo.toml b/Cargo.toml index 6b2e0e94a1..4211ed404e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index caecf5041f..ebaf90cc8c 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -91,6 +91,7 @@ shardtree.workspace = true # - Test dependencies proptest = { workspace = true, optional = true } jubjub = { workspace = true, optional = true } +ambassador = { workspace = true, optional = true } # - ZIP 321 nom = "7" @@ -195,6 +196,7 @@ tor = [ ## Exposes APIs that are useful for testing, such as `proptest` strategies. test-dependencies = [ + "dep:ambassador", "dep:proptest", "dep:jubjub", "orchard?/test-dependencies", diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 302380d42c..7c6525723b 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -103,7 +103,7 @@ use { }; #[cfg(any(test, feature = "test-dependencies"))] -use zcash_primitives::consensus::NetworkUpgrade; +use {ambassador::delegatable_trait, zcash_primitives::consensus::NetworkUpgrade}; pub mod chain; pub mod error; @@ -662,12 +662,22 @@ impl SpendableNotes { self.sapling.as_ref() } + /// Consumes this value and returns the Sapling notes contained within it. + pub fn take_sapling(self) -> Vec> { + self.sapling + } + /// Returns the set of spendable Orchard notes. #[cfg(feature = "orchard")] pub fn orchard(&self) -> &[ReceivedNote] { self.orchard.as_ref() } + /// Consumes this value and returns the Orchard notes contained within it. + pub fn take_orchard(self) -> Vec> { + self.orchard + } + /// Computes the total value of Sapling notes. pub fn sapling_value(&self) -> Result { self.sapling @@ -721,6 +731,7 @@ impl SpendableNotes { /// 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; @@ -798,6 +809,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; @@ -1773,6 +1785,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; @@ -1974,9 +1987,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; From db6b9708eb4bd78ed68f07138eb1e04ffb1ed4fe Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 5 Sep 2024 14:02:49 -0600 Subject: [PATCH 04/20] zcash_client_sqlite: Generalize the test framework to enable it to be moved to `zcash_client_backend` --- zcash_client_backend/src/data_api.rs | 1 + zcash_client_sqlite/Cargo.toml | 1 + zcash_client_sqlite/src/lib.rs | 144 ++++--- zcash_client_sqlite/src/testing.rs | 369 +++++++++++------- zcash_client_sqlite/src/testing/db.rs | 116 ++++++ zcash_client_sqlite/src/testing/pool.rs | 255 ++++++------ zcash_client_sqlite/src/wallet.rs | 36 +- zcash_client_sqlite/src/wallet/init.rs | 18 +- zcash_client_sqlite/src/wallet/orchard.rs | 60 +-- zcash_client_sqlite/src/wallet/sapling.rs | 55 +-- zcash_client_sqlite/src/wallet/scanning.rs | 117 +++--- zcash_client_sqlite/src/wallet/transparent.rs | 28 +- 12 files changed, 743 insertions(+), 457 deletions(-) create mode 100644 zcash_client_sqlite/src/testing/db.rs diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 7c6525723b..a52ecb55c7 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -674,6 +674,7 @@ impl SpendableNotes { } /// Consumes this value and returns the Orchard notes contained within it. + #[cfg(feature = "orchard")] pub fn take_orchard(self) -> Vec> { self.orchard } diff --git a/zcash_client_sqlite/Cargo.toml b/zcash_client_sqlite/Cargo.toml index 25f2288c51..c795b31928 100644 --- a/zcash_client_sqlite/Cargo.toml +++ b/zcash_client_sqlite/Cargo.toml @@ -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"] } diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 52f136cbbd..0e0695c17b 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -264,28 +264,36 @@ impl, P: consensus::Parameters> InputSource for &self, account: AccountId, target_value: NonNegativeAmount, - _sources: &[ShieldedProtocol], + sources: &[ShieldedProtocol], anchor_height: BlockHeight, exclude: &[Self::NoteRef], ) -> Result, 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![] + }, )) } @@ -1687,10 +1695,11 @@ mod tests { }; use zcash_keys::keys::{UnifiedFullViewingKey, UnifiedSpendingKey}; use zcash_primitives::block::BlockHash; + use zcash_protocol::consensus; use crate::{ error::SqliteClientError, - testing::{TestBuilder, TestState}, + testing::{db::TestDbFactory, TestBuilder, TestState}, AccountId, DEFAULT_UA_REQUEST, }; @@ -1703,13 +1712,14 @@ mod tests { #[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 +1734,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 +1742,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 +1775,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 +1805,19 @@ mod tests { AccountSource::Derived { seed_fingerprint: _, account_index } if account_index == zip32_index_2); } - fn check_collisions( - st: &mut TestState, + fn check_collisions( + st: &mut TestState, ufvk: &UnifiedFullViewingKey, birthday: &AccountBirthday, - existing_id: AccountId, - ) { + _existing_id: AccountId, + ) 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(_) + ); // 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 +1832,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(_) + ); } // Remove the Orchard component so that we don't have a match on the full UFVK. @@ -1837,17 +1854,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(_) + ); } } #[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, ); @@ -1869,18 +1893,19 @@ mod tests { #[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 +1913,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!( @@ -1908,10 +1933,12 @@ mod tests { #[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, ); @@ -1933,6 +1960,7 @@ mod tests { fn transparent_receivers() { // Add an account to the wallet. let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -1940,10 +1968,7 @@ mod tests { 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( @@ -1964,7 +1989,10 @@ mod tests { use zcash_primitives::consensus::NetworkConstants; use zcash_primitives::zip32; - let mut st = TestBuilder::new().with_fs_block_cache().build(); + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) + .with_fs_block_cache() + .build(); // The BlockMeta DB starts off empty. assert_eq!(st.cache().get_max_cached_height().unwrap(), None); @@ -1972,7 +2000,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, diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 2c64592442..2d864b9224 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -15,6 +15,7 @@ use rusqlite::{params, Connection}; use secrecy::{Secret, SecretVec}; use shardtree::error::ShardTreeError; +use subtle::ConditionallySelectable; use tempfile::NamedTempFile; #[cfg(feature = "unstable")] @@ -26,7 +27,7 @@ use sapling::{ zip32::DiversifiableFullViewingKey, Note, Nullifier, }; -use zcash_client_backend::data_api::Account as AccountTrait; +use zcash_client_backend::data_api::{Account, InputSource}; #[allow(deprecated)] use zcash_client_backend::{ address::Address, @@ -74,8 +75,7 @@ use crate::{ chain::init::init_cache_database, error::SqliteClientError, wallet::{ - commitment_tree, get_wallet_summary, init::init_wallet_db, sapling::tests::test_prover, - Account, SubtreeScanProgress, + commitment_tree, get_wallet_summary, sapling::tests::test_prover, SubtreeScanProgress, }, AccountId, ReceivedNoteId, WalletDb, }; @@ -102,6 +102,7 @@ use crate::{ FsBlockDb, }; +pub(crate) mod db; pub(crate) mod pool; pub(crate) struct InitialChainState { @@ -111,17 +112,29 @@ pub(crate) struct InitialChainState { pub(crate) prior_orchard_roots: Vec>, } +pub(crate) trait DataStoreFactory { + type Error: core::fmt::Debug; + type AccountId: ConditionallySelectable + Default + Send + 'static; + type DataStore: InputSource + + WalletRead + + WalletWrite + + WalletCommitmentTrees; + + fn new_data_store(&self, network: LocalNetwork) -> Result; +} + /// A builder for a `zcash_client_sqlite` test. -pub(crate) struct TestBuilder { +pub(crate) struct TestBuilder { rng: ChaChaRng, network: LocalNetwork, cache: Cache, + ds_factory: DataStoreFactory, initial_chain_state: Option, account_birthday: Option, account_index: Option, } -impl TestBuilder<()> { +impl TestBuilder<(), ()> { pub const DEFAULT_NETWORK: LocalNetwork = LocalNetwork { overwinter: Some(BlockHeight::from_u32(1)), sapling: Some(BlockHeight::from_u32(100_000)), @@ -134,7 +147,7 @@ impl TestBuilder<()> { z_future: None, }; - /// Constructs a new test. + /// Constructs a new test environment builder. pub(crate) fn new() -> Self { TestBuilder { rng: ChaChaRng::seed_from_u64(0), @@ -142,18 +155,22 @@ impl TestBuilder<()> { // We pick 100,000 to be large enough to handle any hard-coded test offsets. network: Self::DEFAULT_NETWORK, cache: (), + ds_factory: (), initial_chain_state: None, account_birthday: None, account_index: None, } } +} +impl TestBuilder<(), A> { /// Adds a [`BlockDb`] cache to the test. - pub(crate) fn with_block_cache(self) -> TestBuilder { + pub(crate) fn with_block_cache(self) -> TestBuilder { TestBuilder { rng: self.rng, network: self.network, cache: BlockCache::new(), + ds_factory: self.ds_factory, initial_chain_state: self.initial_chain_state, account_birthday: self.account_birthday, account_index: self.account_index, @@ -162,11 +179,12 @@ impl TestBuilder<()> { /// Adds a [`FsBlockDb`] cache to the test. #[cfg(feature = "unstable")] - pub(crate) fn with_fs_block_cache(self) -> TestBuilder { + pub(crate) fn with_fs_block_cache(self) -> TestBuilder { TestBuilder { rng: self.rng, network: self.network, cache: FsBlockCache::new(), + ds_factory: self.ds_factory, initial_chain_state: self.initial_chain_state, account_birthday: self.account_birthday, account_index: self.account_index, @@ -174,7 +192,24 @@ impl TestBuilder<()> { } } -impl TestBuilder { +impl TestBuilder { + pub(crate) fn with_data_store_factory( + self, + ds_factory: DsFactory, + ) -> TestBuilder { + TestBuilder { + rng: self.rng, + network: self.network, + cache: self.cache, + ds_factory, + initial_chain_state: self.initial_chain_state, + account_birthday: self.account_birthday, + account_index: self.account_index, + } + } +} + +impl TestBuilder { pub(crate) fn with_initial_chain_state( mut self, chain_state: impl FnOnce(&mut ChaChaRng, &LocalNetwork) -> InitialChainState, @@ -222,20 +257,19 @@ impl TestBuilder { self.account_index = Some(index); self } +} +impl TestBuilder { /// Builds the state for this test. - pub(crate) fn build(self) -> TestState { - let data_file = NamedTempFile::new().unwrap(); - let mut db_data = WalletDb::for_path(data_file.path(), self.network).unwrap(); - init_wallet_db(&mut db_data, None).unwrap(); - + pub(crate) fn build(self) -> TestState { let mut cached_blocks = BTreeMap::new(); + let mut wallet_data = self.ds_factory.new_data_store(self.network).unwrap(); if let Some(initial_state) = &self.initial_chain_state { - db_data + wallet_data .put_sapling_subtree_roots(0, &initial_state.prior_sapling_roots) .unwrap(); - db_data + wallet_data .with_sapling_tree_mut(|t| { t.insert_frontier( initial_state.chain_state.final_sapling_tree().clone(), @@ -249,10 +283,10 @@ impl TestBuilder { #[cfg(feature = "orchard")] { - db_data + wallet_data .put_orchard_subtree_roots(0, &initial_state.prior_orchard_roots) .unwrap(); - db_data + wallet_data .with_orchard_tree_mut(|t| { t.insert_frontier( initial_state.chain_state.final_orchard_tree().clone(), @@ -285,10 +319,15 @@ impl TestBuilder { let test_account = self.account_birthday.map(|birthday| { let seed = Secret::new(vec![0u8; 32]); let (account, usk) = match self.account_index { - Some(index) => db_data.import_account_hd(&seed, index, &birthday).unwrap(), + Some(index) => wallet_data + .import_account_hd(&seed, index, &birthday) + .unwrap(), None => { - let result = db_data.create_account(&seed, &birthday).unwrap(); - (db_data.get_account(result.0).unwrap().unwrap(), result.1) + let result = wallet_data.create_account(&seed, &birthday).unwrap(); + ( + wallet_data.get_account(result.0).unwrap().unwrap(), + result.1, + ) } }; ( @@ -307,8 +346,8 @@ impl TestBuilder { latest_block_height: self .initial_chain_state .map(|s| s.chain_state.block_height()), - _data_file: data_file, - db_data, + wallet_data, + network: self.network, test_account, rng: self.rng, } @@ -395,21 +434,17 @@ impl CachedBlock { } #[derive(Clone)] -pub(crate) struct TestAccount { - account: Account, +pub(crate) struct TestAccount { + account: A, usk: UnifiedSpendingKey, birthday: AccountBirthday, } -impl TestAccount { - pub(crate) fn account(&self) -> &Account { +impl TestAccount { + pub(crate) fn account(&self) -> &A { &self.account } - pub(crate) fn account_id(&self) -> AccountId { - self.account.id() - } - pub(crate) fn usk(&self) -> &UnifiedSpendingKey { &self.usk } @@ -419,19 +454,119 @@ impl TestAccount { } } +impl Account for TestAccount { + type AccountId = A::AccountId; + + fn id(&self) -> Self::AccountId { + self.account.id() + } + + fn source(&self) -> data_api::AccountSource { + self.account.source() + } + + fn ufvk(&self) -> Option<&zcash_keys::keys::UnifiedFullViewingKey> { + self.account.ufvk() + } + + fn uivk(&self) -> zcash_keys::keys::UnifiedIncomingViewingKey { + self.account.uivk() + } +} + +pub(crate) trait Reset: WalletRead + Sized { + type Handle; + + fn reset(st: &mut TestState) -> Self::Handle; +} + /// The state for a `zcash_client_sqlite` test. -pub(crate) struct TestState { +pub(crate) struct TestState { cache: Cache, cached_blocks: BTreeMap, latest_block_height: Option, - _data_file: NamedTempFile, - db_data: WalletDb, - test_account: Option<(SecretVec, TestAccount)>, + wallet_data: DataStore, + network: Network, + test_account: Option<(SecretVec, TestAccount)>, rng: ChaChaRng, } -impl TestState +impl TestState { + /// Exposes an immutable reference to the test's `DataStore`. + pub(crate) fn wallet(&self) -> &DataStore { + &self.wallet_data + } + + /// Exposes a mutable reference to the test's `DataStore`. + pub(crate) fn wallet_mut(&mut self) -> &mut DataStore { + &mut self.wallet_data + } + + /// Exposes the test framework's source of randomness. + pub(crate) fn rng_mut(&mut self) -> &mut ChaChaRng { + &mut self.rng + } + + /// Exposes the network in use. + pub(crate) fn network(&self) -> &Network { + &self.network + } +} + +impl + TestState +{ + /// Convenience method for obtaining the Sapling activation height for the network under test. + pub(crate) fn sapling_activation_height(&self) -> BlockHeight { + self.network + .activation_height(NetworkUpgrade::Sapling) + .expect("Sapling activation height must be known.") + } + + /// Convenience method for obtaining the NU5 activation height for the network under test. + #[allow(dead_code)] + pub(crate) fn nu5_activation_height(&self) -> BlockHeight { + self.network + .activation_height(NetworkUpgrade::Nu5) + .expect("NU5 activation height must be known.") + } + + /// Exposes the test seed, if enabled via [`TestBuilder::with_test_account`]. + pub(crate) fn test_seed(&self) -> Option<&SecretVec> { + self.test_account.as_ref().map(|(seed, _)| seed) + } +} + +impl TestState where + Network: consensus::Parameters, + DataStore: WalletRead, +{ + /// Exposes the test account, if enabled via [`TestBuilder::with_test_account`]. + pub(crate) fn test_account(&self) -> Option<&TestAccount<::Account>> { + self.test_account.as_ref().map(|(_, acct)| acct) + } + + /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. + pub(crate) fn test_account_sapling(&self) -> Option<&DiversifiableFullViewingKey> { + let (_, acct) = self.test_account.as_ref()?; + let ufvk = acct.ufvk()?; + ufvk.sapling() + } + + /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. + #[cfg(feature = "orchard")] + pub(crate) fn test_account_orchard(&self) -> Option<&orchard::keys::FullViewingKey> { + let (_, acct) = self.test_account.as_ref()?; + let ufvk = acct.ufvk()?; + ufvk.orchard() + } +} + +impl TestState +where + Network: consensus::Parameters, + DataStore: WalletWrite, ::Error: fmt::Debug, { /// Exposes an immutable reference to the test's [`BlockSource`]. @@ -461,7 +596,6 @@ where ); self.cache.insert(&compact_block) } - /// Creates a fake block at the expected next height containing a single output of the /// given value, and inserts it into the cache. pub(crate) fn generate_next_block( @@ -613,7 +747,7 @@ where } let (cb, nfs) = fake_compact_block( - &self.network(), + &self.network, height, prev_hash, outputs, @@ -645,7 +779,7 @@ where let height = prior_cached_block.height() + 1; let cb = fake_compact_block_spending( - &self.network(), + &self.network, height, prior_cached_block.chain_state.block_hash(), note, @@ -717,7 +851,16 @@ where (height, res) } +} +impl TestState +where + Cache: TestCache, + ::Error: fmt::Debug, + ParamsT: consensus::Parameters + Send + 'static, + DbT: WalletWrite, + ::AccountId: ConditionallySelectable + Default + Send + 'static, +{ /// Invokes [`scan_cached_blocks`] with the given arguments, expecting success. pub(crate) fn scan_cached_blocks( &mut self, @@ -736,10 +879,7 @@ where limit: usize, ) -> Result< ScanSummary, - data_api::chain::error::Error< - SqliteClientError, - ::Error, - >, + data_api::chain::error::Error::Error>, > { let prior_cached_block = self .latest_cached_block_below_height(from_height) @@ -747,30 +887,28 @@ where .unwrap_or_else(|| CachedBlock::none(from_height - 1)); let result = scan_cached_blocks( - &self.network(), + &self.network, self.cache.block_source(), - &mut self.db_data, + &mut self.wallet_data, from_height, &prior_cached_block.chain_state, limit, ); result } +} +impl TestState { /// Resets the wallet using a new wallet database but with the same cache of blocks, /// and returns the old wallet database file. /// /// This does not recreate accounts, nor does it rescan the cached blocks. /// The resulting wallet has no test account. /// Before using any `generate_*` method on the reset state, call `reset_latest_cached_block()`. - pub(crate) fn reset(&mut self) -> NamedTempFile { - let network = self.network(); + pub(crate) fn reset(&mut self) -> DbT::Handle { self.latest_block_height = None; - let tf = std::mem::replace(&mut self._data_file, NamedTempFile::new().unwrap()); - self.db_data = WalletDb::for_path(self._data_file.path(), network).unwrap(); self.test_account = None; - init_wallet_db(&mut self.db_data, None).unwrap(); - tf + DbT::reset(self) } // /// Reset the latest cached block to the most recent one in the cache database. @@ -792,69 +930,7 @@ where // } } -impl TestState { - /// Exposes an immutable reference to the test's [`WalletDb`]. - pub(crate) fn wallet(&self) -> &WalletDb { - &self.db_data - } - - /// Exposes a mutable reference to the test's [`WalletDb`]. - pub(crate) fn wallet_mut(&mut self) -> &mut WalletDb { - &mut self.db_data - } - - /// Exposes the test framework's source of randomness. - pub(crate) fn rng_mut(&mut self) -> &mut ChaChaRng { - &mut self.rng - } - - /// Exposes the network in use. - pub(crate) fn network(&self) -> LocalNetwork { - self.db_data.params - } - - /// Convenience method for obtaining the Sapling activation height for the network under test. - pub(crate) fn sapling_activation_height(&self) -> BlockHeight { - self.db_data - .params - .activation_height(NetworkUpgrade::Sapling) - .expect("Sapling activation height must be known.") - } - - /// Convenience method for obtaining the NU5 activation height for the network under test. - #[allow(dead_code)] - pub(crate) fn nu5_activation_height(&self) -> BlockHeight { - self.db_data - .params - .activation_height(NetworkUpgrade::Nu5) - .expect("NU5 activation height must be known.") - } - - /// Exposes the test seed, if enabled via [`TestBuilder::with_test_account`]. - pub(crate) fn test_seed(&self) -> Option<&SecretVec> { - self.test_account.as_ref().map(|(seed, _)| seed) - } - - /// Exposes the test account, if enabled via [`TestBuilder::with_test_account`]. - pub(crate) fn test_account(&self) -> Option<&TestAccount> { - self.test_account.as_ref().map(|(_, acct)| acct) - } - - /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. - pub(crate) fn test_account_sapling(&self) -> Option { - self.test_account - .as_ref() - .and_then(|(_, acct)| acct.usk.to_unified_full_viewing_key().sapling().cloned()) - } - - /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. - #[cfg(feature = "orchard")] - pub(crate) fn test_account_orchard(&self) -> Option { - self.test_account - .as_ref() - .and_then(|(_, acct)| acct.usk.to_unified_full_viewing_key().orchard().cloned()) - } - +impl TestState { /// Insert shard roots for both trees. pub(crate) fn put_subtree_roots( &mut self, @@ -896,11 +972,10 @@ impl TestState { Zip317FeeError, >, > { - let params = self.network(); let prover = test_prover(); create_spend_to_address( - &mut self.db_data, - ¶ms, + self.wallet_data.db_mut(), + &self.network, &prover, &prover, usk, @@ -936,11 +1011,10 @@ impl TestState { InputsT: InputSelector>, { #![allow(deprecated)] - let params = self.network(); let prover = test_prover(); spend( - &mut self.db_data, - ¶ms, + self.wallet_data.db_mut(), + &self.network, &prover, &prover, input_selector, @@ -971,10 +1045,9 @@ impl TestState { where InputsT: InputSelector>, { - let params = self.network(); propose_transfer::<_, _, _, Infallible>( - &mut self.db_data, - ¶ms, + self.wallet_data.db_mut(), + &self.network, spend_from_account, input_selector, request, @@ -1004,10 +1077,9 @@ impl TestState { Zip317FeeError, >, > { - let params = self.network(); let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>( - &mut self.db_data, - ¶ms, + self.wallet_data.db_mut(), + &self.network, fee_rule, spend_from_account, min_confirmations, @@ -1019,7 +1091,7 @@ impl TestState { ); if let Ok(proposal) = &result { - check_proposal_serialization_roundtrip(self.wallet(), proposal); + check_proposal_serialization_roundtrip(self.wallet_data.db(), proposal); } result @@ -1047,10 +1119,9 @@ impl TestState { where InputsT: ShieldingSelector>, { - let params = self.network(); propose_shielding::<_, _, _, Infallible>( - &mut self.db_data, - ¶ms, + self.wallet_data.db_mut(), + &self.network, input_selector, shielding_threshold, from_addrs, @@ -1076,11 +1147,10 @@ impl TestState { where FeeRuleT: FeeRule, { - let params = self.network(); let prover = test_prover(); create_proposed_transactions( - &mut self.db_data, - ¶ms, + self.wallet_data.db_mut(), + &self.network, &prover, &prover, usk, @@ -1111,11 +1181,10 @@ impl TestState { where InputsT: ShieldingSelector>, { - let params = self.network(); let prover = test_prover(); shield_transparent_funds( - &mut self.db_data, - ¶ms, + self.wallet_data.db_mut(), + &self.network, &prover, &prover, input_selector, @@ -1177,8 +1246,8 @@ impl TestState { min_confirmations: u32, ) -> Option> { get_wallet_summary( - &self.wallet().conn.unchecked_transaction().unwrap(), - &self.wallet().params, + &self.wallet().conn().unchecked_transaction().unwrap(), + &self.network, min_confirmations, &SubtreeScanProgress, ) @@ -1199,7 +1268,7 @@ impl TestState { pub(crate) fn get_tx_history( &self, ) -> Result>, SqliteClientError> { - let mut stmt = self.wallet().conn.prepare_cached( + let mut stmt = self.wallet().conn().prepare_cached( "SELECT * FROM v_transactions ORDER BY mined_height DESC, tx_index DESC", @@ -1239,7 +1308,7 @@ impl TestState { pub(crate) fn get_checkpoint_history( &self, ) -> Result)>, SqliteClientError> { - let mut stmt = self.wallet().conn.prepare_cached( + let mut stmt = self.wallet().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 @@ -1276,7 +1345,10 @@ impl TestState { 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}""#)); + run_sqlite3( + self.wallet_data.data_file().path(), + &format!(r#".dump "{name}""#), + ); } } @@ -1290,7 +1362,7 @@ impl TestState { #[allow(dead_code)] #[cfg(feature = "unstable")] pub(crate) unsafe fn run_sqlite3(&self, command: &str) { - run_sqlite3(self._data_file.path(), command) + run_sqlite3(self.wallet_data.data_file().path(), command) } } @@ -2145,14 +2217,11 @@ impl TestCache for FsBlockCache { } } -pub(crate) fn input_selector( +pub(crate) fn input_selector( fee_rule: StandardFeeRule, change_memo: Option<&str>, fallback_change_pool: ShieldedProtocol, -) -> GreedyInputSelector< - WalletDb, - standard::SingleOutputChangeStrategy, -> { +) -> GreedyInputSelector, standard::SingleOutputChangeStrategy> { let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::().unwrap())); let change_strategy = standard::SingleOutputChangeStrategy::new(fee_rule, change_memo, fallback_change_pool); @@ -2161,11 +2230,11 @@ pub(crate) fn input_selector( // Checks that a protobuf proposal serialized from the provided proposal value correctly parses to // the same proposal value. -fn check_proposal_serialization_roundtrip( - db_data: &WalletDb, +fn check_proposal_serialization_roundtrip( + wallet_data: &WalletDb, proposal: &Proposal, ) { let proposal_proto = proposal::Proposal::from_standard_proposal(proposal); - let deserialized_proposal = proposal_proto.try_into_standard_proposal(db_data); + let deserialized_proposal = proposal_proto.try_into_standard_proposal(wallet_data); assert_matches!(deserialized_proposal, Ok(r) if &r == proposal); } diff --git a/zcash_client_sqlite/src/testing/db.rs b/zcash_client_sqlite/src/testing/db.rs new file mode 100644 index 0000000000..7147df9ed3 --- /dev/null +++ b/zcash_client_sqlite/src/testing/db.rs @@ -0,0 +1,116 @@ +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, + *, + }, + 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 super::{DataStoreFactory, Reset, TestState}; +use crate::{wallet::init::init_wallet_db, AccountId, WalletDb}; + +#[cfg(feature = "transparent-inputs")] +use { + core::ops::Range, + crate::TransparentAddressMetadata, + 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, + data_file: NamedTempFile, +} + +impl TestDb { + pub(crate) fn from_parts( + wallet_db: WalletDb, + data_file: NamedTempFile, + ) -> Self { + Self { + wallet_db, + data_file, + } + } + + pub(crate) fn db(&self) -> &WalletDb { + &self.wallet_db + } + + pub(crate) fn db_mut(&mut self) -> &mut WalletDb { + &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 + } + + #[cfg(feature = "unstable")] + pub(crate) fn data_file(&self) -> &NamedTempFile { + &self.data_file + } + + pub(crate) fn take_data_file(self) -> NamedTempFile { + self.data_file + } +} + +pub(crate) struct TestDbFactory; + +impl DataStoreFactory for TestDbFactory { + type Error = (); + type AccountId = AccountId; + type DataStore = TestDb; + + fn new_data_store(&self, network: LocalNetwork) -> Result { + 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(st: &mut TestState) -> NamedTempFile { + let network = *st.network(); + let old_db = std::mem::replace( + &mut st.wallet_data, + TestDbFactory.new_data_store(network).unwrap(), + ); + old_db.take_data_file() + } +} diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index 242601993a..dec2a07beb 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -37,7 +37,8 @@ use zcash_client_backend::{ decrypt_and_store_transaction, input_selection::{GreedyInputSelector, GreedyInputSelectorError}, }, - AccountBirthday, DecryptedTransaction, Ratio, WalletRead, WalletSummary, WalletWrite, + Account as _, AccountBirthday, DecryptedTransaction, InputSource, Ratio, + WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, }, decrypt_transaction, fees::{fixed, standard, DustOutputPolicy}, @@ -47,16 +48,16 @@ use zcash_client_backend::{ zip321::{self, Payment, TransactionRequest}, ShieldedProtocol, }; -use zcash_protocol::consensus::BlockHeight; +use zcash_protocol::consensus::{self, BlockHeight}; use super::TestFvk; use crate::{ error::SqliteClientError, testing::{ - input_selector, AddressType, BlockCache, FakeCompactOutput, InitialChainState, TestBuilder, - TestState, + db::{TestDb, TestDbFactory}, + input_selector, AddressType, FakeCompactOutput, InitialChainState, TestBuilder, TestState, }, - wallet::{block_max_scanned, commitment_tree, parse_scope, truncate_to_height}, + wallet::{commitment_tree, parse_scope, truncate_to_height}, AccountId, NoteId, ReceivedNoteId, }; @@ -90,7 +91,9 @@ pub(crate) trait ShieldedPoolTester { type MerkleTreeHash; type Note; - fn test_account_fvk(st: &TestState) -> Self::Fvk; + fn test_account_fvk( + st: &TestState, + ) -> 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; @@ -114,21 +117,22 @@ pub(crate) trait ShieldedPoolTester { fn empty_tree_leaf() -> Self::MerkleTreeHash; fn empty_tree_root(level: Level) -> Self::MerkleTreeHash; - fn put_subtree_roots( - st: &mut TestState, + fn put_subtree_roots( + st: &mut TestState, start_index: u64, roots: &[CommitmentTreeRoot], - ) -> Result<(), ShardTreeError>; + ) -> Result<(), ShardTreeError<::Error>>; fn next_subtree_index(s: &WalletSummary) -> u64; - fn select_spendable_notes( - st: &TestState, - account: AccountId, + #[allow(clippy::type_complexity)] + fn select_spendable_notes( + st: &TestState, + account: ::AccountId, target_value: NonNegativeAmount, anchor_height: BlockHeight, - exclude: &[ReceivedNoteId], - ) -> Result>, SqliteClientError>; + exclude: &[DbT::NoteRef], + ) -> Result>, ::Error>; fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, AccountId>) -> usize; @@ -137,8 +141,8 @@ pub(crate) trait ShieldedPoolTester { f: impl FnMut(&MemoBytes), ); - fn try_output_recovery( - st: &TestState, + fn try_output_recovery( + params: &P, height: BlockHeight, tx: &Transaction, fvk: &Self::Fvk, @@ -149,6 +153,7 @@ pub(crate) trait ShieldedPoolTester { pub(crate) fn send_single_step_proposed_transfer() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -162,11 +167,12 @@ pub(crate) fn send_single_step_proposed_transfer() { st.scan_cached_blocks(h, 1); // Spendable balance matches total balance - assert_eq!(st.get_total_balance(account.account_id()), value); - assert_eq!(st.get_spendable_balance(account.account_id(), 1), value); + assert_eq!(st.get_total_balance(account.id()), value); + assert_eq!(st.get_spendable_balance(account.id(), 1), value); assert_eq!( - block_max_scanned(&st.wallet().conn, &st.wallet().params) + st.wallet() + .block_max_scanned() .unwrap() .unwrap() .block_height(), @@ -176,7 +182,7 @@ pub(crate) fn send_single_step_proposed_transfer() { 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()), + to.to_zcash_address(st.network()), NonNegativeAmount::const_from_u64(10000), )]) .unwrap(); @@ -193,7 +199,7 @@ pub(crate) fn send_single_step_proposed_transfer() { let proposal = st .propose_transfer( - account.account_id(), + account.id(), input_selector, request, NonZeroU32::new(1).unwrap(), @@ -215,13 +221,10 @@ pub(crate) fn send_single_step_proposed_transfer() { .get_transaction(sent_tx_id) .unwrap() .expect("Created transaction was stored."); - let ufvks = [( - account.account_id(), - account.usk().to_unified_full_viewing_key(), - )] - .into_iter() - .collect(); - let d_tx = decrypt_transaction(&st.network(), h + 1, &tx, &ufvks); + 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; @@ -241,7 +244,7 @@ pub(crate) fn send_single_step_proposed_transfer() { let sent_note_ids = { let mut stmt_sent_notes = st .wallet() - .conn + .conn() .prepare( "SELECT output_index FROM sent_notes @@ -294,8 +297,9 @@ pub(crate) fn send_single_step_proposed_transfer() { let tx_history = st.get_tx_history().unwrap(); assert_eq!(tx_history.len(), 2); + let network = *st.network(); assert_matches!( - decrypt_and_store_transaction(&st.network(), st.wallet_mut(), &tx, None), + decrypt_and_store_transaction(&network, st.wallet_mut(), &tx, None), Ok(_) ); } @@ -321,21 +325,23 @@ pub(crate) fn send_multi_step_proposed_transfer() { }; let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); let account = st.test_account().cloned().unwrap(); - let account_id = account.account_id(); + let account_id = account.id(); let (default_addr, default_index) = account.usk().default_transparent_address(); let dfvk = T::test_account_fvk(&st); - let add_funds = |st: &mut TestState<_>, value| { + let add_funds = |st: &mut TestState<_, TestDb, _>, value| { let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); st.scan_cached_blocks(h, 1); assert_eq!( - block_max_scanned(&st.wallet().conn, &st.wallet().params) + st.wallet() + .block_max_scanned() .unwrap() .unwrap() .block_height(), @@ -348,7 +354,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { let value = NonNegativeAmount::const_from_u64(100000); let transfer_amount = NonNegativeAmount::const_from_u64(50000); - let run_test = |st: &mut TestState<_>, expected_index| { + let run_test = |st: &mut TestState<_, TestDb, _>, expected_index| { // Add funds to the wallet. add_funds(st, value); @@ -407,7 +413,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { // Verify that the stored sent outputs match what we're expecting. let mut stmt_sent = st .wallet() - .conn + .conn() .prepare( "SELECT value, to_address, ephemeral_addresses.address, ephemeral_addresses.address_index FROM sent_notes @@ -459,7 +465,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { assert_matches!( confirmed_sent[1][0].clone(), (sent_v, sent_to_addr, None, None) - if sent_v == u64::try_from(transfer_amount).unwrap() && sent_to_addr == Some(tex_addr.encode(&st.wallet().params))); + if sent_v == u64::try_from(transfer_amount).unwrap() && sent_to_addr == Some(tex_addr.encode(st.network()))); // Check that the transaction history matches what we expect. let tx_history = st.get_tx_history().unwrap(); @@ -501,7 +507,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { let height = add_funds(&mut st, value); - let ephemeral_taddr = Address::decode(&st.wallet().params, &ephemeral0).expect("valid address"); + let ephemeral_taddr = Address::decode(st.network(), &ephemeral0).expect("valid address"); assert_matches!( ephemeral_taddr, Address::Transparent(TransparentAddress::PublicKeyHash(_)) @@ -555,7 +561,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { } let mut builder = Builder::new( - st.wallet().params, + *st.network(), height + 1, BuildConfig::Standard { sapling_anchor: None, @@ -614,7 +620,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { // We call get_wallet_transparent_output with `allow_unspendable = true` to verify // storage because the decrypted transaction has not yet been mined. let utxo = - get_wallet_transparent_output(&st.db_data.conn, &OutPoint::new(txid.into(), 0), true) + get_wallet_transparent_output(st.wallet().conn(), &OutPoint::new(txid.into(), 0), true) .unwrap(); assert_matches!(utxo, Some(v) if v.value() == utxo_value); @@ -626,7 +632,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { assert_eq!(new_known_addrs.len(), (GAP_LIMIT as usize) + 11); assert!(new_known_addrs.starts_with(&known_addrs)); - let reservation_should_succeed = |st: &mut TestState<_>, n| { + let reservation_should_succeed = |st: &mut TestState<_, TestDb, _>, n| { let reserved = st .wallet_mut() .reserve_next_n_ephemeral_addresses(account_id, n) @@ -634,7 +640,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { assert_eq!(reserved.len(), n); reserved }; - let reservation_should_fail = |st: &mut TestState<_>, n, expected_bad_index| { + let reservation_should_fail = |st: &mut TestState<_, TestDb, _>, n, expected_bad_index| { assert_matches!(st .wallet_mut() .reserve_next_n_ephemeral_addresses(account_id, n), @@ -724,20 +730,22 @@ pub(crate) fn proposal_fails_if_not_all_ephemeral_outputs_consumed, value| { + let add_funds = |st: &mut TestState<_, TestDb, _>, value| { let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); st.scan_cached_blocks(h, 1); assert_eq!( - block_max_scanned(&st.wallet().conn, &st.wallet().params) + st.wallet() + .block_max_scanned() .unwrap() .unwrap() .block_height(), @@ -806,6 +814,7 @@ pub(crate) fn proposal_fails_if_not_all_ephemeral_outputs_consumed() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); let dfvk = T::test_account_fvk(&st); @@ -813,7 +822,7 @@ pub(crate) fn create_to_address_fails_on_incorrect_usk() // Create a USK that doesn't exist in the wallet let acct1 = zip32::AccountId::try_from(1).unwrap(); - let usk1 = UnifiedSpendingKey::from_seed(&st.network(), &[1u8; 32], acct1).unwrap(); + let usk1 = UnifiedSpendingKey::from_seed(st.network(), &[1u8; 32], acct1).unwrap(); // Attempting to spend with a USK that is not in the wallet results in an error assert_matches!( @@ -834,10 +843,11 @@ pub(crate) fn create_to_address_fails_on_incorrect_usk() #[allow(deprecated)] pub(crate) fn proposal_fails_with_no_blocks() { let mut 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 dfvk = T::test_account_fvk(&st); let to = T::fvk_default_address(&dfvk); @@ -862,12 +872,13 @@ pub(crate) fn proposal_fails_with_no_blocks() { pub(crate) fn spend_fails_on_unverified_notes() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); let account = st.test_account().cloned().unwrap(); - let account_id = account.account_id(); + let account_id = account.id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet in a single note @@ -1013,12 +1024,13 @@ pub(crate) fn spend_fails_on_unverified_notes() { pub(crate) fn spend_fails_on_locked_notes() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); let account = st.test_account().cloned().unwrap(); - let account_id = account.account_id(); + let account_id = account.id(); let dfvk = T::test_account_fvk(&st); let fee_rule = StandardFeeRule::Zip317; @@ -1148,12 +1160,13 @@ pub(crate) fn spend_fails_on_locked_notes() { pub(crate) fn ovk_policy_prevents_recovery_from_chain() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); let account = st.test_account().cloned().unwrap(); - let account_id = account.account_id(); + let account_id = account.id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet in a single note @@ -1171,7 +1184,7 @@ pub(crate) fn ovk_policy_prevents_recovery_from_chain() { let fee_rule = StandardFeeRule::Zip317; #[allow(clippy::type_complexity)] - let send_and_recover_with_policy = |st: &mut TestState, + let send_and_recover_with_policy = |st: &mut TestState<_, TestDb, _>, ovk_policy| -> Result< Option<(Note, Address, MemoBytes)>, @@ -1200,17 +1213,16 @@ pub(crate) fn ovk_policy_prevents_recovery_from_chain() { // Fetch the transaction from the database let raw_tx: Vec<_> = st .wallet() - .conn + .conn() .query_row( - "SELECT raw FROM transactions - WHERE txid = ?", + "SELECT raw FROM transactions WHERE txid = ?", [txid.as_ref()], |row| row.get(0), ) .unwrap(); let tx = Transaction::read(&raw_tx[..], BranchId::Canopy).unwrap(); - T::try_output_recovery(st, h1, &tx, &dfvk) + T::try_output_recovery(st.network(), h1, &tx, &dfvk) }; // Send some of the funds to another address, keeping history. @@ -1241,12 +1253,13 @@ pub(crate) fn ovk_policy_prevents_recovery_from_chain() { pub(crate) fn spend_succeeds_to_t_addr_zero_change() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); let account = st.test_account().cloned().unwrap(); - let account_id = account.account_id(); + let account_id = account.id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet in a single note @@ -1285,12 +1298,13 @@ pub(crate) fn spend_succeeds_to_t_addr_zero_change() { pub(crate) fn change_note_spends_succeed() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); let account = st.test_account().cloned().unwrap(); - let account_id = account.account_id(); + let account_id = account.id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet in a single note owned by the internal spending key @@ -1309,7 +1323,7 @@ pub(crate) fn change_note_spends_succeed() { NonNegativeAmount::ZERO ); - let change_note_scope = st.wallet().conn.query_row( + let change_note_scope = st.wallet().conn().query_row( &format!( "SELECT recipient_key_scope FROM {}_received_notes @@ -1349,11 +1363,14 @@ pub(crate) fn change_note_spends_succeed() { pub(crate) fn external_address_change_spends_detected_in_restore_from_seed< T: ShieldedPoolTester, >() { - let mut st = TestBuilder::new().with_block_cache().build(); + let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) + .with_block_cache() + .build(); // Add two accounts to the wallet. let seed = Secret::new([0u8; 32].to_vec()); - let birthday = AccountBirthday::from_sapling_activation(&st.network(), BlockHash([0; 32])); + let birthday = AccountBirthday::from_sapling_activation(st.network(), BlockHash([0; 32])); let (account_id, usk) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); let dfvk = T::sk_to_fvk(T::usk_to_sk(&usk)); @@ -1376,9 +1393,9 @@ pub(crate) fn external_address_change_spends_detected_in_restore_from_seed< let addr2 = T::fvk_default_address(&dfvk2); let req = TransactionRequest::new(vec![ // payment to an external recipient - Payment::without_memo(addr2.to_zcash_address(&st.network()), amount_sent), + Payment::without_memo(addr2.to_zcash_address(st.network()), amount_sent), // payment back to the originating wallet, simulating legacy change - Payment::without_memo(addr.to_zcash_address(&st.network()), amount_legacy_change), + Payment::without_memo(addr.to_zcash_address(st.network()), amount_legacy_change), ]) .unwrap(); @@ -1437,12 +1454,13 @@ pub(crate) fn external_address_change_spends_detected_in_restore_from_seed< #[allow(dead_code)] pub(crate) fn zip317_spend() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); let account = st.test_account().cloned().unwrap(); - let account_id = account.account_id(); + let account_id = account.id(); let dfvk = T::test_account_fvk(&st); // Add funds to the wallet @@ -1472,7 +1490,7 @@ pub(crate) fn zip317_spend() { // This first request will fail due to insufficient non-dust funds let req = TransactionRequest::new(vec![Payment::without_memo( - T::fvk_default_address(&dfvk).to_zcash_address(&st.network()), + T::fvk_default_address(&dfvk).to_zcash_address(st.network()), NonNegativeAmount::const_from_u64(50000), )]) .unwrap(); @@ -1493,7 +1511,7 @@ pub(crate) fn zip317_spend() { // This request will succeed, spending a single dust input to pay the 10000 // ZAT fee in addition to the 41000 ZAT output to the recipient let req = TransactionRequest::new(vec![Payment::without_memo( - T::fvk_default_address(&dfvk).to_zcash_address(&st.network()), + T::fvk_default_address(&dfvk).to_zcash_address(st.network()), NonNegativeAmount::const_from_u64(41000), )]) .unwrap(); @@ -1523,6 +1541,7 @@ pub(crate) fn zip317_spend() { #[cfg(feature = "transparent-inputs")] pub(crate) fn shield_transparent() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -1532,7 +1551,7 @@ pub(crate) fn shield_transparent() { let uaddr = st .wallet() - .get_current_address(account.account_id()) + .get_current_address(account.id()) .unwrap() .unwrap(); let taddr = uaddr.transparent().unwrap(); @@ -1596,6 +1615,7 @@ pub(crate) fn birthday_in_anchor_shard() { // notes beyond the end of the first shard. let frontier_tree_size: u32 = (0x1 << 16) + 1234; let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_initial_chain_state(|rng, network| { let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + 1000; @@ -1671,7 +1691,7 @@ pub(crate) fn birthday_in_anchor_shard() { // Verify that the received note is not considered spendable let account = st.test_account().unwrap(); - let account_id = account.account_id(); + let account_id = account.id(); let spendable = T::select_spendable_notes( &st, account_id, @@ -1701,6 +1721,7 @@ pub(crate) fn birthday_in_anchor_shard() { pub(crate) fn checkpoint_gaps() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -1738,14 +1759,14 @@ pub(crate) fn checkpoint_gaps() { // Fake that everything has been scanned st.wallet() - .conn + .conn() .execute_batch("UPDATE scan_queue SET priority = 10") .unwrap(); // Verify that our note is considered spendable let spendable = T::select_spendable_notes( &st, - account.account_id(), + account.id(), NonNegativeAmount::const_from_u64(300000), account.birthday().height() + 5, &[], @@ -1773,6 +1794,7 @@ pub(crate) fn checkpoint_gaps() { #[cfg(feature = "orchard")] pub(crate) fn pool_crossing_required() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard // activation after Sapling @@ -1790,15 +1812,12 @@ pub(crate) fn pool_crossing_required() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard // activation after Sapling @@ -1880,15 +1900,12 @@ pub(crate) fn fully_funded_fully_private() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard // activation after Sapling @@ -1970,15 +1988,12 @@ pub(crate) fn fully_funded_send_to_t() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard // activation after Sapling .build(); let account = st.test_account().cloned().unwrap(); - let acct_id = account.account_id(); + let acct_id = account.id(); let p0_fvk = P0::test_account_fvk(&st); let p1_fvk = P1::test_account_fvk(&st); @@ -2089,7 +2105,7 @@ pub(crate) fn multi_pool_checkpoint() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard // activation after Sapling @@ -2253,6 +2270,7 @@ pub(crate) fn multi_pool_checkpoints_with_pruning< pub(crate) fn valid_chain_states() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2287,6 +2305,7 @@ pub(crate) fn valid_chain_states() { #[allow(dead_code)] pub(crate) fn invalid_chain_cache_disconnected() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2341,6 +2360,7 @@ pub(crate) fn invalid_chain_cache_disconnected() { pub(crate) fn data_db_truncation() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2362,46 +2382,46 @@ pub(crate) fn data_db_truncation() { // Spendable balance should reflect both received notes assert_eq!( - st.get_spendable_balance(account.account_id(), 1), + st.get_spendable_balance(account.id(), 1), (value + value2).unwrap() ); // "Rewind" to height of last scanned block (this is a no-op) st.wallet_mut() + .db_mut() .transactionally(|wdb| truncate_to_height(wdb.conn.0, &wdb.params, h + 1)) .unwrap(); // Spendable balance should be unaltered assert_eq!( - st.get_spendable_balance(account.account_id(), 1), + st.get_spendable_balance(account.id(), 1), (value + value2).unwrap() ); // Rewind so that one block is dropped st.wallet_mut() + .db_mut() .transactionally(|wdb| truncate_to_height(wdb.conn.0, &wdb.params, h)) .unwrap(); // Spendable balance should only contain the first received note; // the rest should be pending. - assert_eq!(st.get_spendable_balance(account.account_id(), 1), value); - assert_eq!( - st.get_pending_shielded_balance(account.account_id(), 1), - value2 - ); + assert_eq!(st.get_spendable_balance(account.id(), 1), value); + assert_eq!(st.get_pending_shielded_balance(account.id(), 1), value2); // Scan the cache again st.scan_cached_blocks(h, 2); // Account balance should again reflect both received notes assert_eq!( - st.get_spendable_balance(account.account_id(), 1), + st.get_spendable_balance(account.id(), 1), (value + value2).unwrap() ); } pub(crate) fn scan_cached_blocks_allows_blocks_out_of_order() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2412,7 +2432,7 @@ pub(crate) fn scan_cached_blocks_allows_blocks_out_of_order() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2479,7 +2500,7 @@ pub(crate) fn scan_cached_blocks_finds_received_notes() { assert_eq!(T::received_note_count(&summary), 1); // Account balance should reflect the received note - assert_eq!(st.get_total_balance(account.account_id()), value); + assert_eq!(st.get_total_balance(account.id()), value); // Create a second fake CompactBlock sending more value to the address let value2 = NonNegativeAmount::const_from_u64(7); @@ -2493,7 +2514,7 @@ pub(crate) fn scan_cached_blocks_finds_received_notes() { // Account balance should reflect both received notes assert_eq!( - st.get_total_balance(account.account_id()), + st.get_total_balance(account.id()), (value + value2).unwrap() ); } @@ -2501,6 +2522,7 @@ pub(crate) fn scan_cached_blocks_finds_received_notes() { // TODO: This test can probably be entirely removed, as the following test duplicates it entirely. pub(crate) fn scan_cached_blocks_finds_change_notes() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2520,7 +2542,7 @@ pub(crate) fn scan_cached_blocks_finds_change_notes() { st.scan_cached_blocks(received_height, 1); // Account balance should reflect the received note - assert_eq!(st.get_total_balance(account.account_id()), value); + assert_eq!(st.get_total_balance(account.id()), value); // Create a second fake CompactBlock spending value from the address let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32])); @@ -2533,13 +2555,14 @@ pub(crate) fn scan_cached_blocks_finds_change_notes() { // Account balance should equal the change assert_eq!( - st.get_total_balance(account.account_id()), + st.get_total_balance(account.id()), (value - value2).unwrap() ); } pub(crate) fn scan_cached_blocks_detects_spends_out_of_order() { let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2566,7 +2589,7 @@ pub(crate) fn scan_cached_blocks_detects_spends_out_of_order(dsf: DsF) { let mut st = TestBuilder::new() + .with_data_store_factory(dsf) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); - let block_fully_scanned = |st: &TestState| { + let block_fully_scanned = |st: &TestState<_, DsF::DataStore, _>| { st.wallet() .block_fully_scanned() .unwrap() @@ -3316,13 +3319,14 @@ mod tests { #[test] fn test_account_birthday() { let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .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() ) } diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index ca6c4b52ef..4eb3c6e2bd 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -429,7 +429,11 @@ mod tests { zip32::AccountId, }; - use crate::{testing::TestBuilder, wallet::db, WalletDb, UA_TRANSPARENT}; + use crate::{ + testing::{db::TestDbFactory, TestBuilder}, + wallet::db, + WalletDb, UA_TRANSPARENT, + }; use super::init_wallet_db; @@ -453,7 +457,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 +495,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 +521,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 +537,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 +551,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(); diff --git a/zcash_client_sqlite/src/wallet/orchard.rs b/zcash_client_sqlite/src/wallet/orchard.rs index cbd8b23b4b..8402644be9 100644 --- a/zcash_client_sqlite/src/wallet/orchard.rs +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -393,10 +393,12 @@ pub(crate) mod tests { note_encryption::OrchardDomain, tree::MerkleHashOrchard, }; + use shardtree::error::ShardTreeError; use zcash_client_backend::{ data_api::{ - chain::CommitmentTreeRoot, DecryptedTransaction, WalletCommitmentTrees, WalletSummary, + chain::CommitmentTreeRoot, DecryptedTransaction, InputSource, WalletCommitmentTrees, + WalletRead, WalletSummary, }, wallet::{Note, ReceivedNote}, }; @@ -406,17 +408,20 @@ pub(crate) mod tests { }; use zcash_note_encryption::try_output_recovery_with_ovk; use zcash_primitives::transaction::Transaction; - use zcash_protocol::{consensus::BlockHeight, memo::MemoBytes, ShieldedProtocol}; + use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + value::Zatoshis, + ShieldedProtocol, + }; - use super::select_spendable_orchard_notes; use crate::{ - error::SqliteClientError, testing::{ self, pool::{OutputRecoveryError, ShieldedPoolTester}, TestState, }, - wallet::{commitment_tree, sapling::tests::SaplingPoolTester}, + wallet::sapling::tests::SaplingPoolTester, ORCHARD_TABLES_PREFIX, }; @@ -431,8 +436,10 @@ pub(crate) mod tests { type MerkleTreeHash = MerkleHashOrchard; type Note = orchard::note::Note; - fn test_account_fvk(st: &TestState) -> Self::Fvk { - st.test_account_orchard().unwrap() + fn test_account_fvk( + st: &TestState, + ) -> Self::Fvk { + st.test_account_orchard().unwrap().clone() } fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk { @@ -479,11 +486,11 @@ pub(crate) mod tests { MerkleHashOrchard::empty_root(level) } - fn put_subtree_roots( - st: &mut TestState, + fn put_subtree_roots( + st: &mut TestState, start_index: u64, roots: &[CommitmentTreeRoot], - ) -> Result<(), ShardTreeError> { + ) -> Result<(), ShardTreeError<::Error>> { st.wallet_mut() .put_orchard_subtree_roots(start_index, roots) } @@ -492,22 +499,23 @@ pub(crate) mod tests { s.next_orchard_subtree_index() } - fn select_spendable_notes( - st: &TestState, - account: crate::AccountId, - target_value: zcash_protocol::value::Zatoshis, + fn select_spendable_notes( + st: &TestState, + account: ::AccountId, + target_value: Zatoshis, anchor_height: BlockHeight, - exclude: &[crate::ReceivedNoteId], - ) -> Result>, SqliteClientError> + exclude: &[DbT::NoteRef], + ) -> Result>, ::Error> { - select_spendable_orchard_notes( - &st.wallet().conn, - &st.wallet().params, - account, - target_value, - anchor_height, - exclude, - ) + st.wallet() + .select_spendable_notes( + account, + target_value, + &[ShieldedProtocol::Orchard], + anchor_height, + exclude, + ) + .map(|n| n.take_orchard()) } fn decrypted_pool_outputs_count( @@ -525,8 +533,8 @@ pub(crate) mod tests { } } - fn try_output_recovery( - _: &TestState, + fn try_output_recovery( + _params: &P, _: BlockHeight, tx: &Transaction, fvk: &Self::Fvk, diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs index 1e51df25c3..36c893fe9a 100644 --- a/zcash_client_sqlite/src/wallet/sapling.rs +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -401,6 +401,7 @@ pub(crate) fn put_received_note( #[cfg(test)] pub(crate) mod tests { use incrementalmerkletree::{Hashable, Level}; + use shardtree::error::ShardTreeError; use zcash_proofs::prover::LocalTxProver; @@ -423,22 +424,22 @@ pub(crate) mod tests { use zcash_client_backend::{ address::Address, data_api::{ - chain::CommitmentTreeRoot, DecryptedTransaction, WalletCommitmentTrees, WalletSummary, + chain::CommitmentTreeRoot, DecryptedTransaction, InputSource, WalletCommitmentTrees, + WalletRead, WalletSummary, }, keys::UnifiedSpendingKey, wallet::{Note, ReceivedNote}, ShieldedProtocol, }; + use zcash_protocol::consensus; use crate::{ - error::SqliteClientError, testing::{ self, pool::{OutputRecoveryError, ShieldedPoolTester}, TestState, }, - wallet::{commitment_tree, sapling::select_spendable_sapling_notes}, - AccountId, ReceivedNoteId, SAPLING_TABLES_PREFIX, + AccountId, SAPLING_TABLES_PREFIX, }; pub(crate) struct SaplingPoolTester; @@ -452,8 +453,10 @@ pub(crate) mod tests { type MerkleTreeHash = sapling::Node; type Note = sapling::Note; - fn test_account_fvk(st: &TestState) -> Self::Fvk { - st.test_account_sapling().unwrap() + fn test_account_fvk( + st: &TestState, + ) -> Self::Fvk { + st.test_account_sapling().unwrap().clone() } fn usk_to_sk(usk: &UnifiedSpendingKey) -> &Self::Sk { @@ -488,11 +491,11 @@ pub(crate) mod tests { sapling::Node::empty_root(level) } - fn put_subtree_roots( - st: &mut TestState, + fn put_subtree_roots( + st: &mut TestState, start_index: u64, roots: &[CommitmentTreeRoot], - ) -> Result<(), ShardTreeError> { + ) -> Result<(), ShardTreeError<::Error>> { st.wallet_mut() .put_sapling_subtree_roots(start_index, roots) } @@ -501,21 +504,23 @@ pub(crate) mod tests { s.next_sapling_subtree_index() } - fn select_spendable_notes( - st: &TestState, - account: AccountId, + fn select_spendable_notes( + st: &TestState, + account: ::AccountId, target_value: NonNegativeAmount, anchor_height: BlockHeight, - exclude: &[ReceivedNoteId], - ) -> Result>, SqliteClientError> { - select_spendable_sapling_notes( - &st.wallet().conn, - &st.wallet().params, - account, - target_value, - anchor_height, - exclude, - ) + exclude: &[DbT::NoteRef], + ) -> Result>, ::Error> + { + st.wallet() + .select_spendable_notes( + account, + target_value, + &[ShieldedProtocol::Sapling], + anchor_height, + exclude, + ) + .map(|n| n.take_sapling()) } fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, AccountId>) -> usize { @@ -531,8 +536,8 @@ pub(crate) mod tests { } } - fn try_output_recovery( - st: &TestState, + fn try_output_recovery( + params: &P, height: BlockHeight, tx: &Transaction, fvk: &Self::Fvk, @@ -542,7 +547,7 @@ pub(crate) mod tests { let result = try_sapling_output_recovery( &fvk.to_ovk(Scope::External), output, - zip212_enforcement(&st.network(), height), + zip212_enforcement(params, height), ); if result.is_some() { diff --git a/zcash_client_sqlite/src/wallet/scanning.rs b/zcash_client_sqlite/src/wallet/scanning.rs index 64dede7e9e..9367a6829e 100644 --- a/zcash_client_sqlite/src/wallet/scanning.rs +++ b/zcash_client_sqlite/src/wallet/scanning.rs @@ -594,12 +594,14 @@ 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, + db::{TestDb, TestDbFactory}, + pool::ShieldedPoolTester, + AddressType, BlockCache, FakeCompactOutput, InitialChainState, TestBuilder, TestState, }, wallet::{ sapling::tests::SaplingPoolTester, @@ -646,6 +648,7 @@ pub(crate) mod tests { let initial_height_offset = 310; let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_initial_chain_state(|rng, network| { let sapling_activation_height = @@ -728,7 +731,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 +739,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 +757,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 +774,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,8 +808,14 @@ pub(crate) mod tests { birthday_offset: u32, prior_block_hash: BlockHash, insert_prior_roots: bool, - ) -> (TestState, T::Fvk, AccountBirthday, u32) { + ) -> ( + TestState, + T::Fvk, + AccountBirthday, + u32, + ) { let st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_initial_chain_state(|rng, network| { // We set the Sapling and Orchard frontiers at the birthday height to be @@ -892,7 +901,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 +909,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() + .build(); let sap_active = st.sapling_activation_height(); // Update the chain tip. @@ -912,7 +924,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 +945,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 +990,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 +1034,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,6 +1063,7 @@ 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_data_store_factory(TestDbFactory) .with_block_cache() .with_initial_chain_state(|rng, network| { let birthday_height = @@ -1123,7 +1136,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 +1167,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 +1193,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 +1226,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,6 +1256,7 @@ 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_data_store_factory(TestDbFactory) .with_block_cache() .with_initial_chain_state(|rng, network| { let birthday_height = @@ -1313,7 +1327,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 +1380,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 +1396,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 +1425,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 +1443,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 +1454,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::( &tx, &(BlockHeight::from(150)..BlockHeight::from(160)), @@ -1462,7 +1480,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 +1488,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 +1499,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::( &tx, &(BlockHeight::from(90)..BlockHeight::from(100)), @@ -1505,7 +1525,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,13 +1554,14 @@ pub(crate) mod tests { #[cfg(feature = "orchard")] fn prepare_orchard_block_spanning_test( with_birthday_subtree_root: bool, - ) -> TestState { + ) -> TestState { 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_data_store_factory(TestDbFactory) .with_block_cache() .with_initial_chain_state(|rng, network| { let birthday_height = @@ -1674,6 +1695,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 +1724,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 +1749,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 +1767,7 @@ pub(crate) mod tests { let proposal = st .propose_transfer( - account.account_id(), + account.id(), input_selector, request, NonZeroU32::new(10).unwrap(), @@ -1767,6 +1787,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 +1812,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 +1837,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 +1854,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 +1867,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(), diff --git a/zcash_client_sqlite/src/wallet/transparent.rs b/zcash_client_sqlite/src/wallet/transparent.rs index 47e70ed572..433b3bed99 100644 --- a/zcash_client_sqlite/src/wallet/transparent.rs +++ b/zcash_client_sqlite/src/wallet/transparent.rs @@ -822,11 +822,16 @@ pub(crate) fn queue_transparent_spend_detection( #[cfg(test)] mod tests { - use crate::testing::{AddressType, TestBuilder, TestState}; + use crate::testing::{ + db::{TestDb, TestDbFactory}, + AddressType, TestBuilder, TestState, + }; + use sapling::zip32::ExtendedSpendingKey; use zcash_client_backend::{ data_api::{ - wallet::input_selection::GreedyInputSelector, InputSource, WalletRead, WalletWrite, + wallet::input_selection::GreedyInputSelector, Account as _, InputSource, WalletRead, + WalletWrite, }, encoding::AddressCodec, fees::{fixed, DustOutputPolicy}, @@ -845,11 +850,12 @@ mod tests { 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 +939,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,6 +955,7 @@ mod tests { use zcash_client_backend::ShieldedProtocol; let mut st = TestBuilder::new() + .with_data_store_factory(TestDbFactory) .with_block_cache() .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -956,7 +963,7 @@ mod tests { 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 +978,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 +994,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() From b43f3bf43ddbba67ea1f21bf69f59a268736e8a7 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 5 Sep 2024 15:03:40 -0600 Subject: [PATCH 05/20] zcash_client_backend: Move data_api::testing module into its own file. --- zcash_client_backend/src/data_api.rs | 465 +------------------ zcash_client_backend/src/data_api/testing.rs | 446 ++++++++++++++++++ 2 files changed, 449 insertions(+), 462 deletions(-) create mode 100644 zcash_client_backend/src/data_api/testing.rs diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index a52ecb55c7..68b4db309a 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -110,6 +110,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 @@ -2055,465 +2058,3 @@ pub trait WalletCommitmentTrees { roots: &[CommitmentTreeRoot], ) -> Result<(), ShardTreeError>; } - -#[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_SHARD_HEIGHT * 2 }, - SAPLING_SHARD_HEIGHT, - >, - #[cfg(feature = "orchard")] - pub orchard_tree: ShardTree< - MemoryShardStore, - { 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>, Self::Error> { - Ok(None) - } - - fn select_spendable_notes( - &self, - _account: Self::AccountId, - _target_value: NonNegativeAmount, - _sources: &[ShieldedProtocol], - _anchor_height: BlockHeight, - _exclude: &[Self::NoteRef], - ) -> Result, 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, Self::Error> { - Ok(Vec::new()) - } - - fn get_account( - &self, - _account_id: Self::AccountId, - ) -> Result, Self::Error> { - Ok(None) - } - - fn get_derived_account( - &self, - _seed: &SeedFingerprint, - _account_id: zip32::AccountId, - ) -> Result, Self::Error> { - Ok(None) - } - - fn validate_seed( - &self, - _account_id: Self::AccountId, - _seed: &SecretVec, - ) -> Result { - Ok(false) - } - - fn seed_relevance_to_derived_accounts( - &self, - _seed: &SecretVec, - ) -> Result, Self::Error> { - Ok(SeedRelevance::NoAccounts) - } - - fn get_account_for_ufvk( - &self, - _ufvk: &UnifiedFullViewingKey, - ) -> Result, Self::Error> { - Ok(None) - } - - fn get_current_address( - &self, - _account: Self::AccountId, - ) -> Result, Self::Error> { - Ok(None) - } - - fn get_account_birthday( - &self, - _account: Self::AccountId, - ) -> Result { - Err(()) - } - - fn get_wallet_birthday(&self) -> Result, Self::Error> { - Ok(None) - } - - fn get_wallet_summary( - &self, - _min_confirmations: u32, - ) -> Result>, Self::Error> { - Ok(None) - } - - fn chain_height(&self) -> Result, Self::Error> { - Ok(None) - } - - fn get_block_hash( - &self, - _block_height: BlockHeight, - ) -> Result, Self::Error> { - Ok(None) - } - - fn block_metadata( - &self, - _height: BlockHeight, - ) -> Result, Self::Error> { - Ok(None) - } - - fn block_fully_scanned(&self) -> Result, Self::Error> { - Ok(None) - } - - fn get_max_height_hash(&self) -> Result, Self::Error> { - Ok(None) - } - - fn block_max_scanned(&self) -> Result, Self::Error> { - Ok(None) - } - - fn suggest_scan_ranges(&self) -> Result, Self::Error> { - Ok(vec![]) - } - - fn get_target_and_anchor_heights( - &self, - _min_confirmations: NonZeroU32, - ) -> Result, Self::Error> { - Ok(None) - } - - fn get_min_unspent_height(&self) -> Result, Self::Error> { - Ok(None) - } - - fn get_tx_height(&self, _txid: TxId) -> Result, Self::Error> { - Ok(None) - } - - fn get_unified_full_viewing_keys( - &self, - ) -> Result, Self::Error> { - Ok(HashMap::new()) - } - - fn get_memo(&self, _id_note: NoteId) -> Result, Self::Error> { - Ok(None) - } - - fn get_transaction(&self, _txid: TxId) -> Result, Self::Error> { - Ok(None) - } - - fn get_sapling_nullifiers( - &self, - _query: NullifierQuery, - ) -> Result, Self::Error> { - Ok(Vec::new()) - } - - #[cfg(feature = "orchard")] - fn get_orchard_nullifiers( - &self, - _query: NullifierQuery, - ) -> Result, Self::Error> { - Ok(Vec::new()) - } - - #[cfg(feature = "transparent-inputs")] - fn get_transparent_receivers( - &self, - _account: Self::AccountId, - ) -> Result>, Self::Error> - { - Ok(HashMap::new()) - } - - #[cfg(feature = "transparent-inputs")] - fn get_transparent_balances( - &self, - _account: Self::AccountId, - _max_height: BlockHeight, - ) -> Result, Self::Error> { - Ok(HashMap::new()) - } - - #[cfg(feature = "transparent-inputs")] - fn get_transparent_address_metadata( - &self, - _account: Self::AccountId, - _address: &TransparentAddress, - ) -> Result, Self::Error> { - Ok(None) - } - - #[cfg(feature = "transparent-inputs")] - fn get_known_ephemeral_addresses( - &self, - _account: Self::AccountId, - _index_range: Option>, - ) -> Result, Self::Error> { - Ok(vec![]) - } - - #[cfg(feature = "transparent-inputs")] - fn find_account_for_ephemeral_address( - &self, - _address: &TransparentAddress, - ) -> Result, Self::Error> { - Ok(None) - } - - fn transaction_data_requests(&self) -> Result, Self::Error> { - Ok(vec![]) - } - } - - impl WalletWrite for MockWalletDb { - type UtxoRef = u32; - - fn create_account( - &mut self, - seed: &SecretVec, - _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, - _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 { - todo!() - } - - fn get_next_available_address( - &mut self, - _account: Self::AccountId, - _request: UnifiedAddressRequest, - ) -> Result, Self::Error> { - Ok(None) - } - - #[allow(clippy::type_complexity)] - fn put_blocks( - &mut self, - _from_state: &ChainState, - _blocks: Vec>, - ) -> 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, - ) -> Result<(), Self::Error> { - Ok(()) - } - - fn store_transactions_to_be_sent( - &mut self, - _transactions: &[SentTransaction], - ) -> 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 { - Ok(0) - } - - #[cfg(feature = "transparent-inputs")] - fn reserve_next_n_ephemeral_addresses( - &mut self, - _account_id: Self::AccountId, - _n: usize, - ) -> Result, 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; - - fn with_sapling_tree_mut(&mut self, mut callback: F) -> Result - where - for<'a> F: FnMut( - &'a mut ShardTree< - Self::SaplingShardStore<'a>, - { sapling::NOTE_COMMITMENT_TREE_DEPTH }, - SAPLING_SHARD_HEIGHT, - >, - ) -> Result, - E: From>, - { - callback(&mut self.sapling_tree) - } - - fn put_sapling_subtree_roots( - &mut self, - start_index: u64, - roots: &[CommitmentTreeRoot], - ) -> Result<(), ShardTreeError> { - 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>(()) - })?; - - Ok(()) - } - - #[cfg(feature = "orchard")] - type OrchardShardStore<'a> = - MemoryShardStore; - - #[cfg(feature = "orchard")] - fn with_orchard_tree_mut(&mut self, mut callback: F) -> Result - where - for<'a> F: FnMut( - &'a mut ShardTree< - Self::OrchardShardStore<'a>, - { ORCHARD_SHARD_HEIGHT * 2 }, - ORCHARD_SHARD_HEIGHT, - >, - ) -> Result, - E: From>, - { - 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], - ) -> Result<(), ShardTreeError> { - 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>(()) - })?; - - Ok(()) - } - } -} diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs new file mode 100644 index 0000000000..43471b55fe --- /dev/null +++ b/zcash_client_backend/src/data_api/testing.rs @@ -0,0 +1,446 @@ +//! Utilities for testing wallets based upon the [`zcash_client_backend::data_api`] traits. +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_SHARD_HEIGHT * 2 }, + SAPLING_SHARD_HEIGHT, + >, + #[cfg(feature = "orchard")] + pub orchard_tree: ShardTree< + MemoryShardStore, + { 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>, Self::Error> { + Ok(None) + } + + fn select_spendable_notes( + &self, + _account: Self::AccountId, + _target_value: NonNegativeAmount, + _sources: &[ShieldedProtocol], + _anchor_height: BlockHeight, + _exclude: &[Self::NoteRef], + ) -> Result, 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, Self::Error> { + Ok(Vec::new()) + } + + fn get_account( + &self, + _account_id: Self::AccountId, + ) -> Result, Self::Error> { + Ok(None) + } + + fn get_derived_account( + &self, + _seed: &SeedFingerprint, + _account_id: zip32::AccountId, + ) -> Result, Self::Error> { + Ok(None) + } + + fn validate_seed( + &self, + _account_id: Self::AccountId, + _seed: &SecretVec, + ) -> Result { + Ok(false) + } + + fn seed_relevance_to_derived_accounts( + &self, + _seed: &SecretVec, + ) -> Result, Self::Error> { + Ok(SeedRelevance::NoAccounts) + } + + fn get_account_for_ufvk( + &self, + _ufvk: &UnifiedFullViewingKey, + ) -> Result, Self::Error> { + Ok(None) + } + + fn get_current_address( + &self, + _account: Self::AccountId, + ) -> Result, Self::Error> { + Ok(None) + } + + fn get_account_birthday(&self, _account: Self::AccountId) -> Result { + Err(()) + } + + fn get_wallet_birthday(&self) -> Result, Self::Error> { + Ok(None) + } + + fn get_wallet_summary( + &self, + _min_confirmations: u32, + ) -> Result>, Self::Error> { + Ok(None) + } + + fn chain_height(&self) -> Result, Self::Error> { + Ok(None) + } + + fn get_block_hash(&self, _block_height: BlockHeight) -> Result, Self::Error> { + Ok(None) + } + + fn block_metadata(&self, _height: BlockHeight) -> Result, Self::Error> { + Ok(None) + } + + fn block_fully_scanned(&self) -> Result, Self::Error> { + Ok(None) + } + + fn get_max_height_hash(&self) -> Result, Self::Error> { + Ok(None) + } + + fn block_max_scanned(&self) -> Result, Self::Error> { + Ok(None) + } + + fn suggest_scan_ranges(&self) -> Result, Self::Error> { + Ok(vec![]) + } + + fn get_target_and_anchor_heights( + &self, + _min_confirmations: NonZeroU32, + ) -> Result, Self::Error> { + Ok(None) + } + + fn get_min_unspent_height(&self) -> Result, Self::Error> { + Ok(None) + } + + fn get_tx_height(&self, _txid: TxId) -> Result, Self::Error> { + Ok(None) + } + + fn get_unified_full_viewing_keys( + &self, + ) -> Result, Self::Error> { + Ok(HashMap::new()) + } + + fn get_memo(&self, _id_note: NoteId) -> Result, Self::Error> { + Ok(None) + } + + fn get_transaction(&self, _txid: TxId) -> Result, Self::Error> { + Ok(None) + } + + fn get_sapling_nullifiers( + &self, + _query: NullifierQuery, + ) -> Result, Self::Error> { + Ok(Vec::new()) + } + + #[cfg(feature = "orchard")] + fn get_orchard_nullifiers( + &self, + _query: NullifierQuery, + ) -> Result, Self::Error> { + Ok(Vec::new()) + } + + #[cfg(feature = "transparent-inputs")] + fn get_transparent_receivers( + &self, + _account: Self::AccountId, + ) -> Result>, Self::Error> { + Ok(HashMap::new()) + } + + #[cfg(feature = "transparent-inputs")] + fn get_transparent_balances( + &self, + _account: Self::AccountId, + _max_height: BlockHeight, + ) -> Result, Self::Error> { + Ok(HashMap::new()) + } + + #[cfg(feature = "transparent-inputs")] + fn get_transparent_address_metadata( + &self, + _account: Self::AccountId, + _address: &TransparentAddress, + ) -> Result, Self::Error> { + Ok(None) + } + + #[cfg(feature = "transparent-inputs")] + fn get_known_ephemeral_addresses( + &self, + _account: Self::AccountId, + _index_range: Option>, + ) -> Result, Self::Error> { + Ok(vec![]) + } + + #[cfg(feature = "transparent-inputs")] + fn find_account_for_ephemeral_address( + &self, + _address: &TransparentAddress, + ) -> Result, Self::Error> { + Ok(None) + } + + fn transaction_data_requests(&self) -> Result, Self::Error> { + Ok(vec![]) + } +} + +impl WalletWrite for MockWalletDb { + type UtxoRef = u32; + + fn create_account( + &mut self, + seed: &SecretVec, + _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, + _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 { + todo!() + } + + fn get_next_available_address( + &mut self, + _account: Self::AccountId, + _request: UnifiedAddressRequest, + ) -> Result, Self::Error> { + Ok(None) + } + + #[allow(clippy::type_complexity)] + fn put_blocks( + &mut self, + _from_state: &ChainState, + _blocks: Vec>, + ) -> 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, + ) -> Result<(), Self::Error> { + Ok(()) + } + + fn store_transactions_to_be_sent( + &mut self, + _transactions: &[SentTransaction], + ) -> 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 { + Ok(0) + } + + #[cfg(feature = "transparent-inputs")] + fn reserve_next_n_ephemeral_addresses( + &mut self, + _account_id: Self::AccountId, + _n: usize, + ) -> Result, 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; + + fn with_sapling_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::SaplingShardStore<'a>, + { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + SAPLING_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + callback(&mut self.sapling_tree) + } + + fn put_sapling_subtree_roots( + &mut self, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError> { + 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>(()) + })?; + + Ok(()) + } + + #[cfg(feature = "orchard")] + type OrchardShardStore<'a> = MemoryShardStore; + + #[cfg(feature = "orchard")] + fn with_orchard_tree_mut(&mut self, mut callback: F) -> Result + where + for<'a> F: FnMut( + &'a mut ShardTree< + Self::OrchardShardStore<'a>, + { ORCHARD_SHARD_HEIGHT * 2 }, + ORCHARD_SHARD_HEIGHT, + >, + ) -> Result, + E: From>, + { + 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], + ) -> Result<(), ShardTreeError> { + 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>(()) + })?; + + Ok(()) + } +} From acd26d5d53bc5afeec7b87e4a1d68c2f4848bd9b Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 5 Sep 2024 17:26:14 -0600 Subject: [PATCH 06/20] zcash_client_sqlite: Move `TransactionSummary` to `zcash_client_backend` --- zcash_client_backend/src/data_api.rs | 12 ++ zcash_client_backend/src/data_api/testing.rs | 103 +++++++++++++++++ zcash_client_sqlite/src/lib.rs | 8 ++ zcash_client_sqlite/src/testing.rs | 115 +------------------ zcash_client_sqlite/src/testing/pool.rs | 4 +- zcash_client_sqlite/src/wallet.rs | 48 ++++++++ 6 files changed, 176 insertions(+), 114 deletions(-) diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 68b4db309a..7af7ccf961 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -1163,6 +1163,18 @@ 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>; + + /// 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(feature = "test-dependencies")] + fn get_tx_history( + &self, + ) -> Result>, Self::Error> { + Ok(vec![]) + } } /// The relevance of a seed to a given wallet. diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs index 43471b55fe..4f5b7b4082 100644 --- a/zcash_client_backend/src/data_api/testing.rs +++ b/zcash_client_backend/src/data_api/testing.rs @@ -3,6 +3,7 @@ use incrementalmerkletree::Address; use secrecy::{ExposeSecret, SecretVec}; use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; use std::{collections::HashMap, convert::Infallible, num::NonZeroU32}; +use zcash_protocol::value::{ZatBalance, Zatoshis}; use zip32::fingerprint::SeedFingerprint; use zcash_primitives::{ @@ -37,6 +38,108 @@ use { #[cfg(feature = "orchard")] use super::ORCHARD_SHARD_HEIGHT; +pub struct TransactionSummary { + account_id: AccountId, + txid: TxId, + expiry_height: Option, + mined_height: Option, + account_value_delta: ZatBalance, + fee_paid: Option, + spent_note_count: usize, + has_change: bool, + sent_note_count: usize, + received_note_count: usize, + memo_count: usize, + expired_unmined: bool, + is_shielding: bool, +} + +impl TransactionSummary { + pub fn new( + account_id: AccountId, + txid: TxId, + expiry_height: Option, + mined_height: Option, + account_value_delta: ZatBalance, + fee_paid: Option, + spent_note_count: usize, + has_change: bool, + sent_note_count: usize, + received_note_count: usize, + memo_count: usize, + expired_unmined: bool, + is_shielding: bool, + ) -> Self { + Self { + account_id, + txid, + expiry_height, + mined_height, + account_value_delta, + fee_paid, + spent_note_count, + has_change, + sent_note_count, + received_note_count, + memo_count, + expired_unmined, + is_shielding, + } + } + + pub fn account_id(&self) -> &AccountId { + &self.account_id + } + + pub fn txid(&self) -> TxId { + self.txid + } + + pub fn expiry_height(&self) -> Option { + self.expiry_height + } + + pub fn mined_height(&self) -> Option { + self.mined_height + } + + pub fn account_value_delta(&self) -> ZatBalance { + self.account_value_delta + } + + pub fn fee_paid(&self) -> Option { + self.fee_paid + } + + pub fn spent_note_count(&self) -> usize { + self.spent_note_count + } + + pub fn has_change(&self) -> bool { + self.has_change + } + + pub fn sent_note_count(&self) -> usize { + self.sent_note_count + } + + pub fn received_note_count(&self) -> usize { + self.received_note_count + } + + pub fn expired_unmined(&self) -> bool { + self.expired_unmined + } + + pub fn memo_count(&self) -> usize { + self.memo_count + } + + pub fn is_shielding(&self) -> bool { + self.is_shielding + } +} + pub struct MockWalletDb { pub network: Network, pub sapling_tree: ShardTree< diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 0e0695c17b..f5370d6e38 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -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 { @@ -611,6 +614,11 @@ impl, P: consensus::Parameters> WalletRead for W Ok(iter.collect()) } + + #[cfg(feature = "test-dependencies")] + fn get_tx_history(&self) -> Result>, Self::Error> { + wallet::testing::get_tx_history(self.conn.borrow()) + } } impl WalletWrite for WalletDb { diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 2d864b9224..9e7272266e 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -27,6 +27,7 @@ use sapling::{ zip32::DiversifiableFullViewingKey, Note, Nullifier, }; +use zcash_client_backend::data_api::testing::TransactionSummary; use zcash_client_backend::data_api::{Account, InputSource}; #[allow(deprecated)] use zcash_client_backend::{ @@ -69,7 +70,7 @@ use zcash_primitives::{ zip32::DiversifierIndex, }; use zcash_protocol::local_consensus::LocalNetwork; -use zcash_protocol::value::{ZatBalance, Zatoshis}; +use zcash_protocol::value::Zatoshis; use crate::{ chain::init::init_cache_database, @@ -1260,50 +1261,11 @@ impl TestState { &self, txid: TxId, ) -> Result>, SqliteClientError> { - let history = self.get_tx_history()?; + let history = self.wallet().get_tx_history()?; Ok(history.into_iter().find(|tx| tx.txid() == txid)) } /// Returns a vector of transaction summaries - pub(crate) fn get_tx_history( - &self, - ) -> Result>, SqliteClientError> { - let mut stmt = self.wallet().conn().prepare_cached( - "SELECT * - FROM v_transactions - ORDER BY mined_height DESC, tx_index DESC", - )?; - - let results = stmt - .query_and_then::, SqliteClientError, _, _>([], |row| { - Ok(TransactionSummary { - account_id: AccountId(row.get("account_id")?), - txid: TxId::from_bytes(row.get("txid")?), - expiry_height: row - .get::<_, Option>("expiry_height")? - .map(BlockHeight::from), - mined_height: row - .get::<_, Option>("mined_height")? - .map(BlockHeight::from), - account_value_delta: ZatBalance::from_i64(row.get("account_balance_delta")?)?, - fee_paid: row - .get::<_, Option>("fee_paid")? - .map(Zatoshis::from_nonnegative_i64) - .transpose()?, - spent_note_count: row.get("spent_note_count")?, - has_change: row.get("has_change")?, - sent_note_count: row.get("sent_note_count")?, - received_note_count: row.get("received_note_count")?, - memo_count: row.get("memo_count")?, - expired_unmined: row.get("expired_unmined")?, - is_shielding: row.get("is_shielding")?, - }) - })? - .collect::, _>>()?; - - Ok(results) - } - #[allow(dead_code)] // used only for tests that are flagged off by default pub(crate) fn get_checkpoint_history( &self, @@ -1396,77 +1358,6 @@ unsafe fn run_sqlite3>(db_path: S, command: &str) { eprintln!("------"); } -pub(crate) struct TransactionSummary { - account_id: AccountId, - txid: TxId, - expiry_height: Option, - mined_height: Option, - account_value_delta: ZatBalance, - fee_paid: Option, - spent_note_count: usize, - has_change: bool, - sent_note_count: usize, - received_note_count: usize, - memo_count: usize, - expired_unmined: bool, - is_shielding: bool, -} - -#[allow(dead_code)] -impl TransactionSummary { - pub(crate) fn account_id(&self) -> &AccountId { - &self.account_id - } - - pub(crate) fn txid(&self) -> TxId { - self.txid - } - - pub(crate) fn expiry_height(&self) -> Option { - self.expiry_height - } - - pub(crate) fn mined_height(&self) -> Option { - self.mined_height - } - - pub(crate) fn account_value_delta(&self) -> ZatBalance { - self.account_value_delta - } - - pub(crate) fn fee_paid(&self) -> Option { - self.fee_paid - } - - pub(crate) fn spent_note_count(&self) -> usize { - self.spent_note_count - } - - pub(crate) fn has_change(&self) -> bool { - self.has_change - } - - pub(crate) fn sent_note_count(&self) -> usize { - self.sent_note_count - } - - pub(crate) fn received_note_count(&self) -> usize { - self.received_note_count - } - - pub(crate) fn expired_unmined(&self) -> bool { - self.expired_unmined - } - - pub(crate) fn memo_count(&self) -> usize { - self.memo_count - } - - pub(crate) fn is_shielding(&self) -> bool { - self.is_shielding - } -} - /// Trait used by tests that require a full viewing key. pub(crate) trait TestFvk { type Nullifier: Copy; diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index dec2a07beb..b607446c17 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -294,7 +294,7 @@ pub(crate) fn send_single_step_proposed_transfer() { Ok(None) ); - let tx_history = st.get_tx_history().unwrap(); + let tx_history = st.wallet().get_tx_history().unwrap(); assert_eq!(tx_history.len(), 2); let network = *st.network(); @@ -468,7 +468,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { if sent_v == u64::try_from(transfer_amount).unwrap() && sent_to_addr == Some(tex_addr.encode(st.network()))); // Check that the transaction history matches what we expect. - let tx_history = st.get_tx_history().unwrap(); + let tx_history = st.wallet().get_tx_history().unwrap(); let tx_0 = tx_history .iter() diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 11c0852192..456fc8cf09 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -3170,6 +3170,54 @@ pub(crate) fn prune_nullifier_map( Ok(()) } +#[cfg(any(test, feature = "test-dependencies"))] +pub mod testing { + use zcash_client_backend::data_api::testing::TransactionSummary; + use zcash_primitives::transaction::TxId; + use zcash_protocol::{ + consensus::BlockHeight, + value::{ZatBalance, Zatoshis}, + }; + + use crate::{error::SqliteClientError, AccountId}; + + pub(crate) fn get_tx_history( + conn: &rusqlite::Connection, + ) -> Result>, 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::, SqliteClientError, _, _>([], |row| { + Ok(TransactionSummary::new( + AccountId(row.get("account_id")?), + TxId::from_bytes(row.get("txid")?), + row.get::<_, Option>("expiry_height")? + .map(BlockHeight::from), + row.get::<_, Option>("mined_height")? + .map(BlockHeight::from), + ZatBalance::from_i64(row.get("account_balance_delta")?)?, + row.get::<_, Option>("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::, _>>()?; + + Ok(results) + } +} + #[cfg(test)] mod tests { use std::num::NonZeroU32; From 58b464d102358c50f3f2999f6ac56f706237fed4 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 5 Sep 2024 17:31:41 -0600 Subject: [PATCH 07/20] zcash_client_sqlite: Generalize more `TestState` operations. --- zcash_client_backend/src/data_api/testing.rs | 1 + zcash_client_sqlite/src/lib.rs | 2 +- zcash_client_sqlite/src/testing.rs | 204 +++++++++++-------- 3 files changed, 117 insertions(+), 90 deletions(-) diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs index 4f5b7b4082..75c17d8a2a 100644 --- a/zcash_client_backend/src/data_api/testing.rs +++ b/zcash_client_backend/src/data_api/testing.rs @@ -55,6 +55,7 @@ pub struct TransactionSummary { } impl TransactionSummary { + #[allow(clippy::too_many_arguments)] pub fn new( account_id: AccountId, txid: TxId, diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index f5370d6e38..683c75b441 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -615,7 +615,7 @@ impl, P: consensus::Parameters> WalletRead for W Ok(iter.collect()) } - #[cfg(feature = "test-dependencies")] + #[cfg(any(test, feature = "test-dependencies"))] fn get_tx_history(&self) -> Result>, Self::Error> { wallet::testing::get_tx_history(self.conn.borrow()) } diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 9e7272266e..489117889d 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -11,7 +11,7 @@ use nonempty::NonEmpty; use prost::Message; use rand_chacha::ChaChaRng; use rand_core::{CryptoRng, RngCore, SeedableRng}; -use rusqlite::{params, Connection}; +use rusqlite::params; use secrecy::{Secret, SecretVec}; use shardtree::error::ShardTreeError; @@ -75,12 +75,12 @@ use zcash_protocol::value::Zatoshis; use crate::{ chain::init::init_cache_database, error::SqliteClientError, - wallet::{ - commitment_tree, get_wallet_summary, sapling::tests::test_prover, SubtreeScanProgress, - }, - AccountId, ReceivedNoteId, WalletDb, + wallet::{get_wallet_summary, sapling::tests::test_prover, SubtreeScanProgress}, + AccountId, ReceivedNoteId, }; +use self::db::TestDb; + use super::BlockDb; #[cfg(feature = "orchard")] @@ -859,7 +859,7 @@ where Cache: TestCache, ::Error: fmt::Debug, ParamsT: consensus::Parameters + Send + 'static, - DbT: WalletWrite, + DbT: InputSource + WalletWrite + WalletCommitmentTrees, ::AccountId: ConditionallySelectable + Default + Send + 'static, { /// Invokes [`scan_cached_blocks`] with the given arguments, expecting success. @@ -880,7 +880,10 @@ where limit: usize, ) -> Result< ScanSummary, - data_api::chain::error::Error::Error>, + data_api::chain::error::Error< + ::Error, + ::Error, + >, > { let prior_cached_block = self .latest_cached_block_below_height(from_height) @@ -897,41 +900,7 @@ where ); result } -} - -impl TestState { - /// Resets the wallet using a new wallet database but with the same cache of blocks, - /// and returns the old wallet database file. - /// - /// This does not recreate accounts, nor does it rescan the cached blocks. - /// The resulting wallet has no test account. - /// Before using any `generate_*` method on the reset state, call `reset_latest_cached_block()`. - pub(crate) fn reset(&mut self) -> DbT::Handle { - self.latest_block_height = None; - self.test_account = None; - DbT::reset(self) - } - // /// Reset the latest cached block to the most recent one in the cache database. - // #[allow(dead_code)] - // pub(crate) fn reset_latest_cached_block(&mut self) { - // self.cache - // .block_source() - // .with_blocks::<_, Infallible>(None, None, |block: CompactBlock| { - // let chain_metadata = block.chain_metadata.unwrap(); - // self.latest_cached_block = Some(CachedBlock::at( - // BlockHash::from_slice(block.hash.as_slice()), - // BlockHeight::from_u32(block.height.try_into().unwrap()), - // chain_metadata.sapling_commitment_tree_size, - // chain_metadata.orchard_commitment_tree_size, - // )); - // Ok(()) - // }) - // .unwrap(); - // } -} - -impl TestState { /// Insert shard roots for both trees. pub(crate) fn put_subtree_roots( &mut self, @@ -939,7 +908,7 @@ impl TestState { sapling_roots: &[CommitmentTreeRoot], #[cfg(feature = "orchard")] orchard_start_index: u64, #[cfg(feature = "orchard")] orchard_roots: &[CommitmentTreeRoot], - ) -> Result<(), ShardTreeError> { + ) -> Result<(), ShardTreeError<::Error>> { self.wallet_mut() .put_sapling_subtree_roots(sapling_start_index, sapling_roots)?; @@ -949,7 +918,18 @@ impl TestState { Ok(()) } +} +impl TestState +where + ParamsT: consensus::Parameters + Send + 'static, + AccountIdT: std::cmp::Eq + std::hash::Hash, + ErrT: std::fmt::Debug, + DbT: InputSource + + WalletWrite + + WalletCommitmentTrees, + ::AccountId: ConditionallySelectable + Default + Send + 'static, +{ /// Invokes [`create_spend_to_address`] with the given arguments. #[allow(deprecated)] #[allow(clippy::type_complexity)] @@ -967,16 +947,17 @@ impl TestState { ) -> Result< NonEmpty, data_api::error::Error< - SqliteClientError, - commitment_tree::Error, - GreedyInputSelectorError, + ErrT, + ::Error, + GreedyInputSelectorError::NoteRef>, Zip317FeeError, >, > { let prover = test_prover(); + let network = self.network().clone(); create_spend_to_address( - self.wallet_data.db_mut(), - &self.network, + self.wallet_mut(), + &network, &prover, &prover, usk, @@ -1002,20 +983,21 @@ impl TestState { ) -> Result< NonEmpty, data_api::error::Error< - SqliteClientError, - commitment_tree::Error, + ErrT, + ::Error, InputsT::Error, ::Error, >, > where - InputsT: InputSelector>, + InputsT: InputSelector, { #![allow(deprecated)] let prover = test_prover(); + let network = self.network().clone(); spend( - self.wallet_data.db_mut(), - &self.network, + self.wallet_mut(), + &network, &prover, &prover, input_selector, @@ -1030,25 +1012,26 @@ impl TestState { #[allow(clippy::type_complexity)] pub(crate) fn propose_transfer( &mut self, - spend_from_account: AccountId, + spend_from_account: ::AccountId, input_selector: &InputsT, request: zip321::TransactionRequest, min_confirmations: NonZeroU32, ) -> Result< - Proposal, + Proposal::NoteRef>, data_api::error::Error< - SqliteClientError, + ErrT, Infallible, InputsT::Error, ::Error, >, > where - InputsT: InputSelector>, + InputsT: InputSelector, { + let network = self.network().clone(); propose_transfer::<_, _, _, Infallible>( - self.wallet_data.db_mut(), - &self.network, + self.wallet_mut(), + &network, spend_from_account, input_selector, request, @@ -1061,7 +1044,7 @@ impl TestState { #[allow(clippy::too_many_arguments)] pub(crate) fn propose_standard_transfer( &mut self, - spend_from_account: AccountId, + spend_from_account: ::AccountId, fee_rule: StandardFeeRule, min_confirmations: NonZeroU32, to: &Address, @@ -1070,17 +1053,18 @@ impl TestState { change_memo: Option, fallback_change_pool: ShieldedProtocol, ) -> Result< - Proposal, + Proposal::NoteRef>, data_api::error::Error< - SqliteClientError, + ErrT, CommitmentTreeErrT, - GreedyInputSelectorError, + GreedyInputSelectorError::NoteRef>, Zip317FeeError, >, > { + let network = self.network().clone(); let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>( - self.wallet_data.db_mut(), - &self.network, + self.wallet_mut(), + &network, fee_rule, spend_from_account, min_confirmations, @@ -1092,7 +1076,7 @@ impl TestState { ); if let Ok(proposal) = &result { - check_proposal_serialization_roundtrip(self.wallet_data.db(), proposal); + check_proposal_serialization_roundtrip(self.wallet(), proposal); } result @@ -1111,18 +1095,19 @@ impl TestState { ) -> Result< Proposal, data_api::error::Error< - SqliteClientError, + ErrT, Infallible, InputsT::Error, ::Error, >, > where - InputsT: ShieldingSelector>, + InputsT: ShieldingSelector, { + let network = self.network().clone(); propose_shielding::<_, _, _, Infallible>( - self.wallet_data.db_mut(), - &self.network, + self.wallet_mut(), + &network, input_selector, shielding_threshold, from_addrs, @@ -1131,6 +1116,7 @@ impl TestState { } /// Invokes [`create_proposed_transactions`] with the given arguments. + #[allow(clippy::type_complexity)] pub(crate) fn create_proposed_transactions( &mut self, usk: &UnifiedSpendingKey, @@ -1139,8 +1125,8 @@ impl TestState { ) -> Result< NonEmpty, data_api::error::Error< - SqliteClientError, - commitment_tree::Error, + ErrT, + ::Error, InputsErrT, FeeRuleT::Error, >, @@ -1149,9 +1135,10 @@ impl TestState { FeeRuleT: FeeRule, { let prover = test_prover(); + let network = self.network().clone(); create_proposed_transactions( - self.wallet_data.db_mut(), - &self.network, + self.wallet_mut(), + &network, &prover, &prover, usk, @@ -1173,19 +1160,20 @@ impl TestState { ) -> Result< NonEmpty, data_api::error::Error< - SqliteClientError, - commitment_tree::Error, + ErrT, + ::Error, InputsT::Error, ::Error, >, > where - InputsT: ShieldingSelector>, + InputsT: ShieldingSelector, { let prover = test_prover(); + let network = self.network().clone(); shield_transparent_funds( - self.wallet_data.db_mut(), - &self.network, + self.wallet_mut(), + &network, &prover, &prover, input_selector, @@ -1198,21 +1186,25 @@ impl TestState { fn with_account_balance T>( &self, - account: AccountId, + account: AccountIdT, min_confirmations: u32, f: F, ) -> T { - let binding = self.get_wallet_summary(min_confirmations).unwrap(); + let binding = self + .wallet() + .get_wallet_summary(min_confirmations) + .unwrap() + .unwrap(); f(binding.account_balances().get(&account).unwrap()) } - pub(crate) fn get_total_balance(&self, account: AccountId) -> NonNegativeAmount { + pub(crate) fn get_total_balance(&self, account: AccountIdT) -> NonNegativeAmount { self.with_account_balance(account, 0, |balance| balance.total()) } pub(crate) fn get_spendable_balance( &self, - account: AccountId, + account: AccountIdT, min_confirmations: u32, ) -> NonNegativeAmount { self.with_account_balance(account, min_confirmations, |balance| { @@ -1222,7 +1214,7 @@ impl TestState { pub(crate) fn get_pending_shielded_balance( &self, - account: AccountId, + account: AccountIdT, min_confirmations: u32, ) -> NonNegativeAmount { self.with_account_balance(account, min_confirmations, |balance| { @@ -1234,14 +1226,16 @@ impl TestState { #[allow(dead_code)] pub(crate) fn get_pending_change( &self, - account: AccountId, + account: AccountIdT, min_confirmations: u32, ) -> NonNegativeAmount { self.with_account_balance(account, min_confirmations, |balance| { balance.change_pending_confirmation() }) } +} +impl TestState { pub(crate) fn get_wallet_summary( &self, min_confirmations: u32, @@ -1328,6 +1322,38 @@ impl TestState { } } +impl TestState { + /// Resets the wallet using a new wallet database but with the same cache of blocks, + /// and returns the old wallet database file. + /// + /// This does not recreate accounts, nor does it rescan the cached blocks. + /// The resulting wallet has no test account. + /// Before using any `generate_*` method on the reset state, call `reset_latest_cached_block()`. + pub(crate) fn reset(&mut self) -> DbT::Handle { + self.latest_block_height = None; + self.test_account = None; + DbT::reset(self) + } + + // /// Reset the latest cached block to the most recent one in the cache database. + // #[allow(dead_code)] + // pub(crate) fn reset_latest_cached_block(&mut self) { + // self.cache + // .block_source() + // .with_blocks::<_, Infallible>(None, None, |block: CompactBlock| { + // let chain_metadata = block.chain_metadata.unwrap(); + // self.latest_cached_block = Some(CachedBlock::at( + // BlockHash::from_slice(block.hash.as_slice()), + // BlockHeight::from_u32(block.height.try_into().unwrap()), + // chain_metadata.sapling_commitment_tree_size, + // chain_metadata.orchard_commitment_tree_size, + // )); + // Ok(()) + // }) + // .unwrap(); + // } +} + // See the doc comment for `TestState::run_sqlite3` above. // // - `db_path` is the path to the database file. @@ -2108,11 +2134,11 @@ impl TestCache for FsBlockCache { } } -pub(crate) fn input_selector( +pub(crate) fn input_selector( fee_rule: StandardFeeRule, change_memo: Option<&str>, fallback_change_pool: ShieldedProtocol, -) -> GreedyInputSelector, standard::SingleOutputChangeStrategy> { +) -> GreedyInputSelector { let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::().unwrap())); let change_strategy = standard::SingleOutputChangeStrategy::new(fee_rule, change_memo, fallback_change_pool); @@ -2121,9 +2147,9 @@ pub(crate) fn input_selector( // Checks that a protobuf proposal serialized from the provided proposal value correctly parses to // the same proposal value. -fn check_proposal_serialization_roundtrip( - wallet_data: &WalletDb, - proposal: &Proposal, +fn check_proposal_serialization_roundtrip( + wallet_data: &DbT, + proposal: &Proposal, ) { let proposal_proto = proposal::Proposal::from_standard_proposal(proposal); let deserialized_proposal = proposal_proto.try_into_standard_proposal(wallet_data); From 15e124e17c93f360f43140e7320f1a52bb43ecc3 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 5 Sep 2024 17:36:37 -0600 Subject: [PATCH 08/20] zcash_client_sqlite: Generalize `TestState::get_wallet_summary` --- zcash_client_sqlite/src/testing.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 489117889d..5c02f272cf 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -73,10 +73,8 @@ use zcash_protocol::local_consensus::LocalNetwork; use zcash_protocol::value::Zatoshis; use crate::{ - chain::init::init_cache_database, - error::SqliteClientError, - wallet::{get_wallet_summary, sapling::tests::test_prover, SubtreeScanProgress}, - AccountId, ReceivedNoteId, + chain::init::init_cache_database, error::SqliteClientError, + wallet::sapling::tests::test_prover, AccountId, ReceivedNoteId, }; use self::db::TestDb; @@ -1233,22 +1231,16 @@ where balance.change_pending_confirmation() }) } -} -impl TestState { pub(crate) fn get_wallet_summary( &self, min_confirmations: u32, - ) -> Option> { - get_wallet_summary( - &self.wallet().conn().unchecked_transaction().unwrap(), - &self.network, - min_confirmations, - &SubtreeScanProgress, - ) - .unwrap() + ) -> Option> { + self.wallet().get_wallet_summary(min_confirmations).unwrap() } +} +impl TestState { /// Returns a transaction from the history. #[allow(dead_code)] pub(crate) fn get_tx_from_history( From ce59a676e9223e64f8724df8e7f2b57b7d03fb95 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 5 Sep 2024 17:47:11 -0600 Subject: [PATCH 09/20] zcash_client_sqlite: Remove the remainder of the sqlite dependencies from TestState --- zcash_client_sqlite/src/testing.rs | 111 ++---------------------- zcash_client_sqlite/src/testing/db.rs | 65 +++++++++++++- zcash_client_sqlite/src/testing/pool.rs | 4 +- zcash_client_sqlite/src/wallet.rs | 31 +++++++ 4 files changed, 100 insertions(+), 111 deletions(-) diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 5c02f272cf..7b1dcddc19 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -2,11 +2,8 @@ use std::fmt; use std::num::NonZeroU32; use std::{collections::BTreeMap, convert::Infallible}; -#[cfg(feature = "unstable")] -use std::{ffi::OsStr, fs::File}; - use group::ff::Field; -use incrementalmerkletree::{Marking, Position, Retention}; +use incrementalmerkletree::{Marking, Retention}; use nonempty::NonEmpty; use prost::Message; use rand_chacha::ChaChaRng; @@ -19,7 +16,7 @@ use subtle::ConditionallySelectable; use tempfile::NamedTempFile; #[cfg(feature = "unstable")] -use tempfile::TempDir; +use {std::fs::File, tempfile::TempDir}; use sapling::{ note_encryption::{sapling_note_encryption, SaplingDomain}, @@ -72,12 +69,8 @@ use zcash_primitives::{ use zcash_protocol::local_consensus::LocalNetwork; use zcash_protocol::value::Zatoshis; -use crate::{ - chain::init::init_cache_database, error::SqliteClientError, - wallet::sapling::tests::test_prover, AccountId, ReceivedNoteId, -}; - -use self::db::TestDb; +use crate::chain::init::init_cache_database; +use crate::{wallet::sapling::tests::test_prover, ReceivedNoteId}; use super::BlockDb; @@ -1238,80 +1231,16 @@ where ) -> Option> { self.wallet().get_wallet_summary(min_confirmations).unwrap() } -} -impl TestState { /// Returns a transaction from the history. #[allow(dead_code)] pub(crate) fn get_tx_from_history( &self, txid: TxId, - ) -> Result>, SqliteClientError> { + ) -> Result>, ErrT> { let history = self.wallet().get_tx_history()?; Ok(history.into_iter().find(|tx| tx.txid() == txid)) } - - /// 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( - &self, - ) -> Result)>, SqliteClientError> { - let mut stmt = self.wallet().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>(2)?.map(Position::from), - )) - })? - .collect::, _>>()?; - - Ok(results) - } - - /// 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.wallet_data.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.wallet_data.data_file().path(), command) - } } impl TestState { @@ -1346,36 +1275,6 @@ impl TestState { // } } -// 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>(db_path: S, command: &str) { - use std::process::Command; - 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!("------"); -} - /// Trait used by tests that require a full viewing key. pub(crate) trait TestFvk { type Nullifier: Copy; diff --git a/zcash_client_sqlite/src/testing/db.rs b/zcash_client_sqlite/src/testing/db.rs index 7147df9ed3..cb71211845 100644 --- a/zcash_client_sqlite/src/testing/db.rs +++ b/zcash_client_sqlite/src/testing/db.rs @@ -77,14 +77,71 @@ impl TestDb { &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 data_file(&self) -> &NamedTempFile { - &self.data_file + 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}""#)); + } } - pub(crate) fn take_data_file(self) -> NamedTempFile { - self.data_file + /// 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>(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; diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index b607446c17..dfa5dced33 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -2173,6 +2173,8 @@ pub(crate) fn multi_pool_checkpoint)> = [ (99999, None), (100000, Some(0)), @@ -2213,7 +2215,7 @@ pub(crate) fn multi_pool_checkpoint Result)>, 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>(2)?.map(Position::from), + )) + })? + .collect::, _>>()?; + + Ok(results) + } } #[cfg(test)] From 7e9f78bf4613b7a3ca81a2646c0ee36315ab1089 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 6 Sep 2024 13:27:41 -0600 Subject: [PATCH 10/20] zcash_client_sqlite: Generalize `TestBuilder::with_block_cache` --- zcash_client_sqlite/src/lib.rs | 8 ++- zcash_client_sqlite/src/testing.rs | 22 ++------ zcash_client_sqlite/src/testing/pool.rs | 53 ++++++++++--------- zcash_client_sqlite/src/wallet.rs | 8 +-- zcash_client_sqlite/src/wallet/scanning.rs | 12 ++--- zcash_client_sqlite/src/wallet/transparent.rs | 4 +- 6 files changed, 49 insertions(+), 58 deletions(-) diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 683c75b441..8d092686a5 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -1967,9 +1967,11 @@ mod tests { #[test] fn transparent_receivers() { // Add an account to the wallet. + + use crate::testing::BlockCache; let st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); let account = st.test_account().unwrap(); @@ -1997,9 +1999,11 @@ mod tests { use zcash_primitives::consensus::NetworkConstants; use zcash_primitives::zip32; + use crate::testing::FsBlockCache; + let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_fs_block_cache() + .with_block_cache(FsBlockCache::new()) .build(); // The BlockMeta DB starts off empty. diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 7b1dcddc19..7e8ff542c5 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -157,25 +157,11 @@ impl TestBuilder<(), ()> { impl TestBuilder<(), A> { /// Adds a [`BlockDb`] cache to the test. - pub(crate) fn with_block_cache(self) -> TestBuilder { + pub(crate) fn with_block_cache(self, cache: C) -> TestBuilder { TestBuilder { rng: self.rng, network: self.network, - cache: BlockCache::new(), - ds_factory: self.ds_factory, - initial_chain_state: self.initial_chain_state, - account_birthday: self.account_birthday, - account_index: self.account_index, - } - } - - /// Adds a [`FsBlockDb`] cache to the test. - #[cfg(feature = "unstable")] - pub(crate) fn with_fs_block_cache(self) -> TestBuilder { - TestBuilder { - rng: self.rng, - network: self.network, - cache: FsBlockCache::new(), + cache, ds_factory: self.ds_factory, initial_chain_state: self.initial_chain_state, account_birthday: self.account_birthday, @@ -1898,7 +1884,7 @@ pub(crate) struct BlockCache { } impl BlockCache { - fn new() -> Self { + pub(crate) fn new() -> Self { let cache_file = NamedTempFile::new().unwrap(); let db_cache = BlockDb::for_path(cache_file.path()).unwrap(); init_cache_database(&db_cache).unwrap(); @@ -1981,7 +1967,7 @@ pub(crate) struct FsBlockCache { #[cfg(feature = "unstable")] impl FsBlockCache { - fn new() -> Self { + pub(crate) fn new() -> Self { let fsblockdb_root = tempfile::tempdir().unwrap(); let mut db_meta = FsBlockDb::for_path(&fsblockdb_root).unwrap(); init_blockmeta_db(&mut db_meta).unwrap(); diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index dfa5dced33..daaaf41d12 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -55,7 +55,8 @@ use crate::{ error::SqliteClientError, testing::{ db::{TestDb, TestDbFactory}, - input_selector, AddressType, FakeCompactOutput, InitialChainState, TestBuilder, TestState, + input_selector, AddressType, BlockCache, FakeCompactOutput, InitialChainState, TestBuilder, + TestState, }, wallet::{commitment_tree, parse_scope, truncate_to_height}, AccountId, NoteId, ReceivedNoteId, @@ -154,7 +155,7 @@ pub(crate) trait ShieldedPoolTester { pub(crate) fn send_single_step_proposed_transfer() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -326,7 +327,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -731,7 +732,7 @@ pub(crate) fn proposal_fails_if_not_all_ephemeral_outputs_consumed() { pub(crate) fn spend_fails_on_unverified_notes() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -1025,7 +1026,7 @@ pub(crate) fn spend_fails_on_unverified_notes() { pub(crate) fn spend_fails_on_locked_notes() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -1161,7 +1162,7 @@ pub(crate) fn spend_fails_on_locked_notes() { pub(crate) fn ovk_policy_prevents_recovery_from_chain() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -1254,7 +1255,7 @@ pub(crate) fn ovk_policy_prevents_recovery_from_chain() { pub(crate) fn spend_succeeds_to_t_addr_zero_change() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -1299,7 +1300,7 @@ pub(crate) fn spend_succeeds_to_t_addr_zero_change() { pub(crate) fn change_note_spends_succeed() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -1365,7 +1366,7 @@ pub(crate) fn external_address_change_spends_detected_in_restore_from_seed< >() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .build(); // Add two accounts to the wallet. @@ -1455,7 +1456,7 @@ pub(crate) fn external_address_change_spends_detected_in_restore_from_seed< pub(crate) fn zip317_spend() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -1542,7 +1543,7 @@ pub(crate) fn zip317_spend() { pub(crate) fn shield_transparent() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -1616,7 +1617,7 @@ pub(crate) fn birthday_in_anchor_shard() { let frontier_tree_size: u32 = (0x1 << 16) + 1234; let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_initial_chain_state(|rng, network| { let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + 1000; @@ -1722,7 +1723,7 @@ pub(crate) fn birthday_in_anchor_shard() { pub(crate) fn checkpoint_gaps() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -1795,7 +1796,7 @@ pub(crate) fn checkpoint_gaps() { pub(crate) fn pool_crossing_required() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard // activation after Sapling .build(); @@ -1882,7 +1883,7 @@ pub(crate) fn pool_crossing_required() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard // activation after Sapling .build(); @@ -1971,7 +1972,7 @@ pub(crate) fn fully_funded_fully_private() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard // activation after Sapling .build(); @@ -2056,7 +2057,7 @@ pub(crate) fn fully_funded_send_to_t() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard // activation after Sapling .build(); @@ -2242,7 +2243,7 @@ pub(crate) fn multi_pool_checkpoints_with_pruning< >() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard // activation after Sapling .build(); @@ -2273,7 +2274,7 @@ pub(crate) fn multi_pool_checkpoints_with_pruning< pub(crate) fn valid_chain_states() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2308,7 +2309,7 @@ pub(crate) fn valid_chain_states() { pub(crate) fn invalid_chain_cache_disconnected() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2363,7 +2364,7 @@ pub(crate) fn invalid_chain_cache_disconnected() { pub(crate) fn data_db_truncation() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2424,7 +2425,7 @@ pub(crate) fn data_db_truncation() { pub(crate) fn scan_cached_blocks_allows_blocks_out_of_order() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2481,7 +2482,7 @@ pub(crate) fn scan_cached_blocks_allows_blocks_out_of_order() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2525,7 +2526,7 @@ pub(crate) fn scan_cached_blocks_finds_received_notes() { pub(crate) fn scan_cached_blocks_finds_change_notes() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -2565,7 +2566,7 @@ pub(crate) fn scan_cached_blocks_finds_change_notes() { pub(crate) fn scan_cached_blocks_detects_spends_out_of_order() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index ebb4fd5dbf..bf319c42fd 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -3260,8 +3260,8 @@ mod tests { use crate::{ testing::{ - db::TestDbFactory, AddressType, DataStoreFactory, FakeCompactOutput, TestBuilder, - TestState, + db::TestDbFactory, AddressType, BlockCache, DataStoreFactory, FakeCompactOutput, + TestBuilder, TestState, }, AccountId, }; @@ -3339,7 +3339,7 @@ mod tests { fn check_block_fully_scanned(dsf: DsF) { let mut st = TestBuilder::new() .with_data_store_factory(dsf) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); @@ -3399,7 +3399,7 @@ mod tests { fn test_account_birthday() { let st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); diff --git a/zcash_client_sqlite/src/wallet/scanning.rs b/zcash_client_sqlite/src/wallet/scanning.rs index 9367a6829e..10ff63abbe 100644 --- a/zcash_client_sqlite/src/wallet/scanning.rs +++ b/zcash_client_sqlite/src/wallet/scanning.rs @@ -649,7 +649,7 @@ pub(crate) mod tests { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_initial_chain_state(|rng, network| { let sapling_activation_height = network.activation_height(NetworkUpgrade::Sapling).unwrap(); @@ -816,7 +816,7 @@ pub(crate) mod tests { ) { let st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .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. @@ -911,7 +911,7 @@ pub(crate) mod tests { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .build(); let sap_active = st.sapling_activation_height(); @@ -1064,7 +1064,7 @@ pub(crate) mod tests { let frontier_tree_size: u32 = (0x1 << 16) + 1234; let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_initial_chain_state(|rng, network| { let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + birthday_offset; @@ -1257,7 +1257,7 @@ pub(crate) mod tests { let frontier_tree_size: u32 = (0x1 << 16) + 1234; let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_initial_chain_state(|rng, network| { let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + birthday_offset; @@ -1562,7 +1562,7 @@ pub(crate) mod tests { let birthday_tree_size: u32 = (0x1 << 17) - 50; let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_initial_chain_state(|rng, network| { let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + birthday_nu5_offset; diff --git a/zcash_client_sqlite/src/wallet/transparent.rs b/zcash_client_sqlite/src/wallet/transparent.rs index 433b3bed99..fa14de41f0 100644 --- a/zcash_client_sqlite/src/wallet/transparent.rs +++ b/zcash_client_sqlite/src/wallet/transparent.rs @@ -824,7 +824,7 @@ pub(crate) fn queue_transparent_spend_detection( mod tests { use crate::testing::{ db::{TestDb, TestDbFactory}, - AddressType, TestBuilder, TestState, + AddressType, BlockCache, TestBuilder, TestState, }; use sapling::zip32::ExtendedSpendingKey; @@ -956,7 +956,7 @@ mod tests { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) - .with_block_cache() + .with_block_cache(BlockCache::new()) .with_account_from_sapling_activation(BlockHash([0; 32])) .build(); From e55df6c4938d11b2b11bd7512e35ca8b7906006b Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 5 Sep 2024 21:48:07 -0600 Subject: [PATCH 11/20] zcash_client_sqlite: Move `TestState` to `zcash_client_backend` --- Cargo.lock | 2 + zcash_client_backend/Cargo.toml | 23 +- zcash_client_backend/src/data_api.rs | 2 +- zcash_client_backend/src/data_api/testing.rs | 1930 ++++++++++++++++- zcash_client_sqlite/src/lib.rs | 14 +- zcash_client_sqlite/src/testing.rs | 1925 +--------------- zcash_client_sqlite/src/testing/db.rs | 6 +- zcash_client_sqlite/src/testing/pool.rs | 29 +- zcash_client_sqlite/src/wallet.rs | 10 +- zcash_client_sqlite/src/wallet/init.rs | 7 +- zcash_client_sqlite/src/wallet/orchard.rs | 5 +- zcash_client_sqlite/src/wallet/sapling.rs | 11 +- zcash_client_sqlite/src/wallet/scanning.rs | 3 +- zcash_client_sqlite/src/wallet/transparent.rs | 9 +- 14 files changed, 1979 insertions(+), 1997 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b932c2fd63..c81d21f8b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5854,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", diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index ebaf90cc8c..12b5ddd664 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -89,9 +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 } -ambassador = { workspace = true, optional = true } +rand_chacha = { workspace = true, optional = true } +zcash_proofs = { workspace = true, optional = true } # - ZIP 321 nom = "7" @@ -138,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. @@ -165,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 = [ @@ -197,11 +205,16 @@ 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", ] diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 7af7ccf961..6f1ee1a934 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -1169,7 +1169,7 @@ pub trait WalletRead { /// 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(feature = "test-dependencies")] + #[cfg(any(test, feature = "test-dependencies"))] fn get_tx_history( &self, ) -> Result>, Self::Error> { diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs index 75c17d8a2a..a0fabf74a4 100644 --- a/zcash_client_backend/src/data_api/testing.rs +++ b/zcash_client_backend/src/data_api/testing.rs @@ -1,42 +1,84 @@ -//! Utilities for testing wallets based upon the [`zcash_client_backend::data_api`] traits. -use incrementalmerkletree::Address; -use secrecy::{ExposeSecret, SecretVec}; +//! Utilities for testing wallets based upon the [`zcash_client_backend::super`] traits. +use assert_matches::assert_matches; +use core::fmt; +use group::ff::Field; +use incrementalmerkletree::{Marking, Retention}; +use nonempty::NonEmpty; +use rand::{CryptoRng, RngCore, SeedableRng}; +use rand_chacha::ChaChaRng; +use sapling::{ + note_encryption::{sapling_note_encryption, SaplingDomain}, + util::generate_random_rseed, + zip32::DiversifiableFullViewingKey, +}; +use secrecy::{ExposeSecret, Secret, SecretVec}; use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; -use std::{collections::HashMap, convert::Infallible, num::NonZeroU32}; -use zcash_protocol::value::{ZatBalance, Zatoshis}; -use zip32::fingerprint::SeedFingerprint; +use std::{ + collections::{BTreeMap, HashMap}, + convert::Infallible, + num::NonZeroU32, +}; +use subtle::ConditionallySelectable; +use zcash_keys::address::Address; +use zcash_note_encryption::Domain; +use zcash_proofs::prover::LocalTxProver; +use zcash_protocol::{ + consensus::{self, NetworkUpgrade, Parameters as _}, + local_consensus::LocalNetwork, + memo::MemoBytes, + value::{ZatBalance, Zatoshis}, +}; +use zip32::{fingerprint::SeedFingerprint, DiversifierIndex}; use zcash_primitives::{ block::BlockHash, consensus::{BlockHeight, Network}, memo::Memo, - transaction::{components::amount::NonNegativeAmount, Transaction, TxId}, + transaction::{ + components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, + fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule}, + Transaction, TxId, + }, }; use crate::{ address::UnifiedAddress, + fees::{standard, DustOutputPolicy}, keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey}, - wallet::{Note, NoteId, ReceivedNote, WalletTransparentOutput}, + proposal::Proposal, + proto::compact_formats::{ + self, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx, + }, + wallet::{Note, NoteId, OvkPolicy, ReceivedNote, WalletTransparentOutput}, ShieldedProtocol, }; +#[allow(deprecated)] use super::{ - chain::{ChainState, CommitmentTreeRoot}, + chain::{scan_cached_blocks, BlockSource, ChainState, CommitmentTreeRoot, ScanSummary}, scanning::ScanRange, - AccountBirthday, AccountPurpose, BlockMetadata, DecryptedTransaction, InputSource, - NullifierQuery, ScannedBlock, SeedRelevance, SentTransaction, SpendableNotes, - TransactionDataRequest, TransactionStatus, WalletCommitmentTrees, WalletRead, WalletSummary, - WalletWrite, SAPLING_SHARD_HEIGHT, + wallet::{ + create_proposed_transactions, create_spend_to_address, + input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector}, + propose_standard_transfer_to_address, propose_transfer, spend, + }, + Account, AccountBalance, AccountBirthday, AccountPurpose, AccountSource, 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, + super::wallet::input_selection::ShieldingSelector, crate::wallet::TransparentAddressMetadata, + std::ops::Range, zcash_primitives::legacy::TransparentAddress, }; #[cfg(feature = "orchard")] -use super::ORCHARD_SHARD_HEIGHT; +use { + super::ORCHARD_SHARD_HEIGHT, crate::proto::compact_formats::CompactOrchardAction, + group::ff::PrimeField, orchard::tree::MerkleHashOrchard, pasta_curves::pallas, +}; pub struct TransactionSummary { account_id: AccountId, @@ -141,6 +183,1852 @@ impl TransactionSummary { } } +#[derive(Clone, Debug)] +pub struct CachedBlock { + chain_state: ChainState, + sapling_end_size: u32, + orchard_end_size: u32, +} + +impl CachedBlock { + pub fn none(sapling_activation_height: BlockHeight) -> Self { + Self { + chain_state: ChainState::empty(sapling_activation_height, BlockHash([0; 32])), + sapling_end_size: 0, + orchard_end_size: 0, + } + } + + 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, + sapling_end_size + ); + #[cfg(feature = "orchard")] + assert_eq!( + chain_state.final_orchard_tree().tree_size() as u32, + orchard_end_size + ); + + Self { + chain_state, + sapling_end_size, + orchard_end_size, + } + } + + fn roll_forward(&self, cb: &CompactBlock) -> Self { + assert_eq!(self.chain_state.block_height() + 1, cb.height()); + + let sapling_final_tree = cb.vtx.iter().flat_map(|tx| tx.outputs.iter()).fold( + self.chain_state.final_sapling_tree().clone(), + |mut acc, c_out| { + acc.append(sapling::Node::from_cmu(&c_out.cmu().unwrap())); + acc + }, + ); + let sapling_end_size = sapling_final_tree.tree_size() as u32; + + #[cfg(feature = "orchard")] + let orchard_final_tree = cb.vtx.iter().flat_map(|tx| tx.actions.iter()).fold( + self.chain_state.final_orchard_tree().clone(), + |mut acc, c_act| { + acc.append(MerkleHashOrchard::from_cmx(&c_act.cmx().unwrap())); + acc + }, + ); + #[cfg(feature = "orchard")] + let orchard_end_size = orchard_final_tree.tree_size() as u32; + #[cfg(not(feature = "orchard"))] + let orchard_end_size = cb.vtx.iter().fold(self.orchard_end_size, |sz, tx| { + sz + (tx.actions.len() as u32) + }); + + Self { + chain_state: ChainState::new( + cb.height(), + cb.hash(), + sapling_final_tree, + #[cfg(feature = "orchard")] + orchard_final_tree, + ), + sapling_end_size, + orchard_end_size, + } + } + + pub fn height(&self) -> BlockHeight { + self.chain_state.block_height() + } + + pub fn sapling_end_size(&self) -> u32 { + self.sapling_end_size + } + + pub fn orchard_end_size(&self) -> u32 { + self.orchard_end_size + } +} + +#[derive(Clone)] +pub struct TestAccount { + account: A, + usk: UnifiedSpendingKey, + birthday: AccountBirthday, +} + +impl TestAccount { + pub fn account(&self) -> &A { + &self.account + } + + pub fn usk(&self) -> &UnifiedSpendingKey { + &self.usk + } + + pub fn birthday(&self) -> &AccountBirthday { + &self.birthday + } +} + +impl Account for TestAccount { + type AccountId = A::AccountId; + + fn id(&self) -> Self::AccountId { + self.account.id() + } + + fn source(&self) -> AccountSource { + self.account.source() + } + + fn ufvk(&self) -> Option<&zcash_keys::keys::UnifiedFullViewingKey> { + self.account.ufvk() + } + + fn uivk(&self) -> zcash_keys::keys::UnifiedIncomingViewingKey { + self.account.uivk() + } +} + +pub trait Reset: WalletRead + Sized { + type Handle; + + fn reset(st: &mut TestState) -> Self::Handle; +} + +/// The state for a `zcash_client_sqlite` test. +pub struct TestState { + cache: Cache, + cached_blocks: BTreeMap, + latest_block_height: Option, + wallet_data: DataStore, + network: Network, + test_account: Option<(SecretVec, TestAccount)>, + rng: ChaChaRng, +} + +impl TestState { + /// Exposes an immutable reference to the test's `DataStore`. + pub fn wallet(&self) -> &DataStore { + &self.wallet_data + } + + /// Exposes a mutable reference to the test's `DataStore`. + pub fn wallet_mut(&mut self) -> &mut DataStore { + &mut self.wallet_data + } + + /// Exposes the test framework's source of randomness. + pub fn rng_mut(&mut self) -> &mut ChaChaRng { + &mut self.rng + } + + /// Exposes the network in use. + pub fn network(&self) -> &Network { + &self.network + } +} + +impl + TestState +{ + /// Convenience method for obtaining the Sapling activation height for the network under test. + pub fn sapling_activation_height(&self) -> BlockHeight { + self.network + .activation_height(NetworkUpgrade::Sapling) + .expect("Sapling activation height must be known.") + } + + /// Convenience method for obtaining the NU5 activation height for the network under test. + #[allow(dead_code)] + pub fn nu5_activation_height(&self) -> BlockHeight { + self.network + .activation_height(NetworkUpgrade::Nu5) + .expect("NU5 activation height must be known.") + } + + /// Exposes the test seed, if enabled via [`TestBuilder::with_test_account`]. + pub fn test_seed(&self) -> Option<&SecretVec> { + self.test_account.as_ref().map(|(seed, _)| seed) + } +} + +impl TestState +where + Network: consensus::Parameters, + DataStore: WalletRead, +{ + /// Exposes the test account, if enabled via [`TestBuilder::with_test_account`]. + pub fn test_account(&self) -> Option<&TestAccount<::Account>> { + self.test_account.as_ref().map(|(_, acct)| acct) + } + + /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. + pub fn test_account_sapling(&self) -> Option<&DiversifiableFullViewingKey> { + let (_, acct) = self.test_account.as_ref()?; + let ufvk = acct.ufvk()?; + ufvk.sapling() + } + + /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. + #[cfg(feature = "orchard")] + pub fn test_account_orchard(&self) -> Option<&orchard::keys::FullViewingKey> { + let (_, acct) = self.test_account.as_ref()?; + let ufvk = acct.ufvk()?; + ufvk.orchard() + } +} + +impl TestState +where + Network: consensus::Parameters, + DataStore: WalletWrite, + ::Error: fmt::Debug, +{ + /// Exposes an immutable reference to the test's [`BlockSource`]. + #[cfg(feature = "unstable")] + pub fn cache(&self) -> &Cache::BlockSource { + self.cache.block_source() + } + + pub fn latest_cached_block(&self) -> Option<&CachedBlock> { + self.latest_block_height + .as_ref() + .and_then(|h| self.cached_blocks.get(h)) + } + + fn latest_cached_block_below_height(&self, height: BlockHeight) -> Option<&CachedBlock> { + self.cached_blocks.range(..height).last().map(|(_, b)| b) + } + + fn cache_block( + &mut self, + prev_block: &CachedBlock, + compact_block: CompactBlock, + ) -> Cache::InsertResult { + self.cached_blocks.insert( + compact_block.height(), + prev_block.roll_forward(&compact_block), + ); + self.cache.insert(&compact_block) + } + /// Creates a fake block at the expected next height containing a single output of the + /// given value, and inserts it into the cache. + pub fn generate_next_block( + &mut self, + fvk: &Fvk, + address_type: AddressType, + value: NonNegativeAmount, + ) -> (BlockHeight, Cache::InsertResult, Fvk::Nullifier) { + let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1); + let prior_cached_block = self.latest_cached_block().unwrap_or(&pre_activation_block); + let height = prior_cached_block.height() + 1; + + let (res, nfs) = self.generate_block_at( + height, + prior_cached_block.chain_state.block_hash(), + &[FakeCompactOutput::new(fvk, address_type, value)], + prior_cached_block.sapling_end_size, + prior_cached_block.orchard_end_size, + false, + ); + + (height, res, nfs[0]) + } + + /// Creates a fake block at the expected next height containing multiple outputs + /// and inserts it into the cache. + #[allow(dead_code)] + pub fn generate_next_block_multi( + &mut self, + outputs: &[FakeCompactOutput], + ) -> (BlockHeight, Cache::InsertResult, Vec) { + let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1); + let prior_cached_block = self.latest_cached_block().unwrap_or(&pre_activation_block); + let height = prior_cached_block.height() + 1; + + let (res, nfs) = self.generate_block_at( + height, + prior_cached_block.chain_state.block_hash(), + outputs, + prior_cached_block.sapling_end_size, + prior_cached_block.orchard_end_size, + false, + ); + + (height, res, nfs) + } + + /// Adds an empty block to the cache, advancing the simulated chain height. + #[allow(dead_code)] // used only for tests that are flagged off by default + pub fn generate_empty_block(&mut self) -> (BlockHeight, Cache::InsertResult) { + let new_hash = { + let mut hash = vec![0; 32]; + self.rng.fill_bytes(&mut hash); + hash + }; + + let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1); + let prior_cached_block = self + .latest_cached_block() + .unwrap_or(&pre_activation_block) + .clone(); + let new_height = prior_cached_block.height() + 1; + + let mut cb = CompactBlock { + hash: new_hash, + height: new_height.into(), + ..Default::default() + }; + cb.prev_hash + .extend_from_slice(&prior_cached_block.chain_state.block_hash().0); + + cb.chain_metadata = Some(compact_formats::ChainMetadata { + sapling_commitment_tree_size: prior_cached_block.sapling_end_size, + orchard_commitment_tree_size: prior_cached_block.orchard_end_size, + }); + + let res = self.cache_block(&prior_cached_block, cb); + self.latest_block_height = Some(new_height); + + (new_height, res) + } + + /// Creates a fake block with the given height and hash containing the requested outputs, and + /// inserts it into the cache. + /// + /// This generated block will be treated as the latest block, and subsequent calls to + /// [`Self::generate_next_block`] will build on it. + #[allow(clippy::too_many_arguments)] + pub fn generate_block_at( + &mut self, + height: BlockHeight, + prev_hash: BlockHash, + outputs: &[FakeCompactOutput], + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + allow_broken_hash_chain: bool, + ) -> (Cache::InsertResult, Vec) { + let mut prior_cached_block = self + .latest_cached_block_below_height(height) + .cloned() + .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1)); + assert!(prior_cached_block.chain_state.block_height() < height); + assert!(prior_cached_block.sapling_end_size <= initial_sapling_tree_size); + assert!(prior_cached_block.orchard_end_size <= initial_orchard_tree_size); + + // If the block height has increased or the Sapling and/or Orchard tree sizes have changed, + // we need to generate a new prior cached block that the block to be generated can + // successfully chain from, with the provided tree sizes. + if prior_cached_block.chain_state.block_height() == height - 1 { + if !allow_broken_hash_chain { + assert_eq!(prev_hash, prior_cached_block.chain_state.block_hash()); + } + } else { + let final_sapling_tree = + (prior_cached_block.sapling_end_size..initial_sapling_tree_size).fold( + prior_cached_block.chain_state.final_sapling_tree().clone(), + |mut acc, _| { + acc.append(sapling::Node::from_scalar(bls12_381::Scalar::random( + &mut self.rng, + ))); + acc + }, + ); + + #[cfg(feature = "orchard")] + let final_orchard_tree = + (prior_cached_block.orchard_end_size..initial_orchard_tree_size).fold( + prior_cached_block.chain_state.final_orchard_tree().clone(), + |mut acc, _| { + acc.append(MerkleHashOrchard::random(&mut self.rng)); + acc + }, + ); + + prior_cached_block = CachedBlock::at( + ChainState::new( + height - 1, + prev_hash, + final_sapling_tree, + #[cfg(feature = "orchard")] + final_orchard_tree, + ), + initial_sapling_tree_size, + initial_orchard_tree_size, + ); + + self.cached_blocks + .insert(height - 1, prior_cached_block.clone()); + } + + let (cb, nfs) = fake_compact_block( + &self.network, + height, + prev_hash, + outputs, + initial_sapling_tree_size, + initial_orchard_tree_size, + &mut self.rng, + ); + assert_eq!(cb.height(), height); + + let res = self.cache_block(&prior_cached_block, cb); + self.latest_block_height = Some(height); + + (res, nfs) + } + + /// Creates a fake block at the expected next height spending the given note, and + /// inserts it into the cache. + pub fn generate_next_block_spending( + &mut self, + fvk: &Fvk, + note: (Fvk::Nullifier, NonNegativeAmount), + to: impl Into
, + value: NonNegativeAmount, + ) -> (BlockHeight, Cache::InsertResult) { + let prior_cached_block = self + .latest_cached_block() + .cloned() + .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1)); + let height = prior_cached_block.height() + 1; + + let cb = fake_compact_block_spending( + &self.network, + height, + prior_cached_block.chain_state.block_hash(), + note, + fvk, + to.into(), + value, + prior_cached_block.sapling_end_size, + prior_cached_block.orchard_end_size, + &mut self.rng, + ); + assert_eq!(cb.height(), height); + + let res = self.cache_block(&prior_cached_block, cb); + self.latest_block_height = Some(height); + + (height, res) + } + + /// Creates a fake block at the expected next height containing only the wallet + /// transaction with the given txid, and inserts it into the cache. + /// + /// This generated block will be treated as the latest block, and subsequent calls to + /// [`Self::generate_next_block`] (or similar) will build on it. + pub fn generate_next_block_including( + &mut self, + txid: TxId, + ) -> (BlockHeight, Cache::InsertResult) { + let tx = self + .wallet() + .get_transaction(txid) + .unwrap() + .expect("TxId should exist in the wallet"); + + // Index 0 is by definition a coinbase transaction, and the wallet doesn't + // construct coinbase transactions. So we pretend here that the block has a + // coinbase transaction that does not have shielded coinbase outputs. + self.generate_next_block_from_tx(1, &tx) + } + + /// Creates a fake block at the expected next height containing only the given + /// transaction, and inserts it into the cache. + /// + /// This generated block will be treated as the latest block, and subsequent calls to + /// [`Self::generate_next_block`] will build on it. + pub fn generate_next_block_from_tx( + &mut self, + tx_index: usize, + tx: &Transaction, + ) -> (BlockHeight, Cache::InsertResult) { + let prior_cached_block = self + .latest_cached_block() + .cloned() + .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1)); + let height = prior_cached_block.height() + 1; + + let cb = fake_compact_block_from_tx( + height, + prior_cached_block.chain_state.block_hash(), + tx_index, + tx, + prior_cached_block.sapling_end_size, + prior_cached_block.orchard_end_size, + &mut self.rng, + ); + assert_eq!(cb.height(), height); + + let res = self.cache_block(&prior_cached_block, cb); + self.latest_block_height = Some(height); + + (height, res) + } +} + +impl TestState +where + Cache: TestCache, + ::Error: fmt::Debug, + ParamsT: consensus::Parameters + Send + 'static, + DbT: InputSource + WalletWrite + WalletCommitmentTrees, + ::AccountId: ConditionallySelectable + Default + Send + 'static, +{ + /// Invokes [`scan_cached_blocks`] with the given arguments, expecting success. + pub fn scan_cached_blocks(&mut self, from_height: BlockHeight, limit: usize) -> ScanSummary { + let result = self.try_scan_cached_blocks(from_height, limit); + assert_matches!(result, Ok(_)); + result.unwrap() + } + + /// Invokes [`scan_cached_blocks`] with the given arguments. + pub fn try_scan_cached_blocks( + &mut self, + from_height: BlockHeight, + limit: usize, + ) -> Result< + ScanSummary, + super::chain::error::Error< + ::Error, + ::Error, + >, + > { + let prior_cached_block = self + .latest_cached_block_below_height(from_height) + .cloned() + .unwrap_or_else(|| CachedBlock::none(from_height - 1)); + + let result = scan_cached_blocks( + &self.network, + self.cache.block_source(), + &mut self.wallet_data, + from_height, + &prior_cached_block.chain_state, + limit, + ); + result + } + + /// Insert shard roots for both trees. + pub fn put_subtree_roots( + &mut self, + sapling_start_index: u64, + sapling_roots: &[CommitmentTreeRoot], + #[cfg(feature = "orchard")] orchard_start_index: u64, + #[cfg(feature = "orchard")] orchard_roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError<::Error>> { + self.wallet_mut() + .put_sapling_subtree_roots(sapling_start_index, sapling_roots)?; + + #[cfg(feature = "orchard")] + self.wallet_mut() + .put_orchard_subtree_roots(orchard_start_index, orchard_roots)?; + + Ok(()) + } +} + +impl TestState +where + ParamsT: consensus::Parameters + Send + 'static, + AccountIdT: std::cmp::Eq + std::hash::Hash, + ErrT: std::fmt::Debug, + DbT: InputSource + + WalletWrite + + WalletCommitmentTrees, + ::AccountId: ConditionallySelectable + Default + Send + 'static, +{ + /// Invokes [`create_spend_to_address`] with the given arguments. + #[allow(deprecated)] + #[allow(clippy::type_complexity)] + #[allow(clippy::too_many_arguments)] + pub fn create_spend_to_address( + &mut self, + usk: &UnifiedSpendingKey, + to: &Address, + amount: NonNegativeAmount, + memo: Option, + ovk_policy: OvkPolicy, + min_confirmations: NonZeroU32, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + ) -> Result< + NonEmpty, + super::error::Error< + ErrT, + ::Error, + GreedyInputSelectorError::NoteRef>, + Zip317FeeError, + >, + > { + let prover = LocalTxProver::bundled(); + let network = self.network().clone(); + create_spend_to_address( + self.wallet_mut(), + &network, + &prover, + &prover, + usk, + to, + amount, + memo, + ovk_policy, + min_confirmations, + change_memo, + fallback_change_pool, + ) + } + + /// Invokes [`spend`] with the given arguments. + #[allow(clippy::type_complexity)] + pub fn spend( + &mut self, + input_selector: &InputsT, + usk: &UnifiedSpendingKey, + request: zip321::TransactionRequest, + ovk_policy: OvkPolicy, + min_confirmations: NonZeroU32, + ) -> Result< + NonEmpty, + super::error::Error< + ErrT, + ::Error, + InputsT::Error, + ::Error, + >, + > + where + InputsT: InputSelector, + { + #![allow(deprecated)] + let prover = LocalTxProver::bundled(); + let network = self.network().clone(); + spend( + self.wallet_mut(), + &network, + &prover, + &prover, + input_selector, + usk, + request, + ovk_policy, + min_confirmations, + ) + } + + /// Invokes [`propose_transfer`] with the given arguments. + #[allow(clippy::type_complexity)] + pub fn propose_transfer( + &mut self, + spend_from_account: ::AccountId, + input_selector: &InputsT, + request: zip321::TransactionRequest, + min_confirmations: NonZeroU32, + ) -> Result< + Proposal::NoteRef>, + super::error::Error::Error>, + > + where + InputsT: InputSelector, + { + let network = self.network().clone(); + propose_transfer::<_, _, _, Infallible>( + self.wallet_mut(), + &network, + spend_from_account, + input_selector, + request, + min_confirmations, + ) + } + + /// Invokes [`propose_standard_transfer`] with the given arguments. + #[allow(clippy::type_complexity)] + #[allow(clippy::too_many_arguments)] + pub fn propose_standard_transfer( + &mut self, + spend_from_account: ::AccountId, + fee_rule: StandardFeeRule, + min_confirmations: NonZeroU32, + to: &Address, + amount: NonNegativeAmount, + memo: Option, + change_memo: Option, + fallback_change_pool: ShieldedProtocol, + ) -> Result< + Proposal::NoteRef>, + super::error::Error< + ErrT, + CommitmentTreeErrT, + GreedyInputSelectorError::NoteRef>, + Zip317FeeError, + >, + > { + let network = self.network().clone(); + let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>( + self.wallet_mut(), + &network, + fee_rule, + spend_from_account, + min_confirmations, + to, + amount, + memo, + change_memo, + fallback_change_pool, + ); + + if let Ok(proposal) = &result { + check_proposal_serialization_roundtrip(self.wallet(), proposal); + } + + result + } + + /// Invokes [`propose_shielding`] with the given arguments. + #[cfg(feature = "transparent-inputs")] + #[allow(clippy::type_complexity)] + #[allow(dead_code)] + pub fn propose_shielding( + &mut self, + input_selector: &InputsT, + shielding_threshold: NonNegativeAmount, + from_addrs: &[TransparentAddress], + min_confirmations: u32, + ) -> Result< + Proposal, + super::error::Error::Error>, + > + where + InputsT: ShieldingSelector, + { + use super::wallet::propose_shielding; + + let network = self.network().clone(); + propose_shielding::<_, _, _, Infallible>( + self.wallet_mut(), + &network, + input_selector, + shielding_threshold, + from_addrs, + min_confirmations, + ) + } + + /// Invokes [`create_proposed_transactions`] with the given arguments. + #[allow(clippy::type_complexity)] + pub fn create_proposed_transactions( + &mut self, + usk: &UnifiedSpendingKey, + ovk_policy: OvkPolicy, + proposal: &Proposal::NoteRef>, + ) -> Result< + NonEmpty, + super::error::Error< + ErrT, + ::Error, + InputsErrT, + FeeRuleT::Error, + >, + > + where + FeeRuleT: FeeRule, + { + let prover = LocalTxProver::bundled(); + let network = self.network().clone(); + create_proposed_transactions( + self.wallet_mut(), + &network, + &prover, + &prover, + usk, + ovk_policy, + proposal, + ) + } + + /// Invokes [`shield_transparent_funds`] with the given arguments. + #[cfg(feature = "transparent-inputs")] + #[allow(clippy::type_complexity)] + pub fn shield_transparent_funds( + &mut self, + input_selector: &InputsT, + shielding_threshold: NonNegativeAmount, + usk: &UnifiedSpendingKey, + from_addrs: &[TransparentAddress], + min_confirmations: u32, + ) -> Result< + NonEmpty, + super::error::Error< + ErrT, + ::Error, + InputsT::Error, + ::Error, + >, + > + where + InputsT: ShieldingSelector, + { + use crate::data_api::wallet::shield_transparent_funds; + + let prover = LocalTxProver::bundled(); + let network = self.network().clone(); + shield_transparent_funds( + self.wallet_mut(), + &network, + &prover, + &prover, + input_selector, + shielding_threshold, + usk, + from_addrs, + min_confirmations, + ) + } + + fn with_account_balance T>( + &self, + account: AccountIdT, + min_confirmations: u32, + f: F, + ) -> T { + let binding = self + .wallet() + .get_wallet_summary(min_confirmations) + .unwrap() + .unwrap(); + f(binding.account_balances().get(&account).unwrap()) + } + + pub fn get_total_balance(&self, account: AccountIdT) -> NonNegativeAmount { + self.with_account_balance(account, 0, |balance| balance.total()) + } + + pub fn get_spendable_balance( + &self, + account: AccountIdT, + min_confirmations: u32, + ) -> NonNegativeAmount { + self.with_account_balance(account, min_confirmations, |balance| { + balance.spendable_value() + }) + } + + pub fn get_pending_shielded_balance( + &self, + account: AccountIdT, + min_confirmations: u32, + ) -> NonNegativeAmount { + self.with_account_balance(account, min_confirmations, |balance| { + balance.value_pending_spendability() + balance.change_pending_confirmation() + }) + .unwrap() + } + + #[allow(dead_code)] + pub fn get_pending_change( + &self, + account: AccountIdT, + min_confirmations: u32, + ) -> NonNegativeAmount { + self.with_account_balance(account, min_confirmations, |balance| { + balance.change_pending_confirmation() + }) + } + + pub fn get_wallet_summary(&self, min_confirmations: u32) -> Option> { + self.wallet().get_wallet_summary(min_confirmations).unwrap() + } + + /// Returns a transaction from the history. + #[allow(dead_code)] + pub fn get_tx_from_history( + &self, + txid: TxId, + ) -> Result>, ErrT> { + let history = self.wallet().get_tx_history()?; + Ok(history.into_iter().find(|tx| tx.txid() == txid)) + } +} + +impl TestState { + /// Resets the wallet using a new wallet database but with the same cache of blocks, + /// and returns the old wallet database file. + /// + /// This does not recreate accounts, nor does it rescan the cached blocks. + /// The resulting wallet has no test account. + /// Before using any `generate_*` method on the reset state, call `reset_latest_cached_block()`. + pub fn reset(&mut self) -> DbT::Handle { + self.latest_block_height = None; + self.test_account = None; + DbT::reset(self) + } + + // /// Reset the latest cached block to the most recent one in the cache database. + // #[allow(dead_code)] + // pub fn reset_latest_cached_block(&mut self) { + // self.cache + // .block_source() + // .with_blocks::<_, Infallible>(None, None, |block: CompactBlock| { + // let chain_metadata = block.chain_metadata.unwrap(); + // self.latest_cached_block = Some(CachedBlock::at( + // BlockHash::from_slice(block.hash.as_slice()), + // BlockHeight::from_u32(block.height.try_into().unwrap()), + // chain_metadata.sapling_commitment_tree_size, + // chain_metadata.orchard_commitment_tree_size, + // )); + // Ok(()) + // }) + // .unwrap(); + // } +} + +pub fn input_selector( + fee_rule: StandardFeeRule, + change_memo: Option<&str>, + fallback_change_pool: ShieldedProtocol, +) -> GreedyInputSelector { + let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::().unwrap())); + let change_strategy = + standard::SingleOutputChangeStrategy::new(fee_rule, change_memo, fallback_change_pool); + GreedyInputSelector::new(change_strategy, DustOutputPolicy::default()) +} + +// Checks that a protobuf proposal serialized from the provided proposal value correctly parses to +// the same proposal value. +fn check_proposal_serialization_roundtrip( + wallet_data: &DbT, + proposal: &Proposal, +) { + let proposal_proto = crate::proto::proposal::Proposal::from_standard_proposal(proposal); + let deserialized_proposal = proposal_proto.try_into_standard_proposal(wallet_data); + assert_matches!(deserialized_proposal, Ok(r) if &r == proposal); +} + +pub struct InitialChainState { + pub chain_state: ChainState, + pub prior_sapling_roots: Vec>, + #[cfg(feature = "orchard")] + pub prior_orchard_roots: Vec>, +} + +pub trait DataStoreFactory { + type Error: core::fmt::Debug; + type AccountId: ConditionallySelectable + Default + Send + 'static; + type DataStore: InputSource + + WalletRead + + WalletWrite + + WalletCommitmentTrees; + + fn new_data_store(&self, network: LocalNetwork) -> Result; +} + +/// A builder for a `zcash_client_sqlite` test. +pub struct TestBuilder { + rng: ChaChaRng, + network: LocalNetwork, + cache: Cache, + ds_factory: DataStoreFactory, + initial_chain_state: Option, + account_birthday: Option, + account_index: Option, +} + +impl TestBuilder<(), ()> { + pub const DEFAULT_NETWORK: LocalNetwork = LocalNetwork { + overwinter: Some(BlockHeight::from_u32(1)), + sapling: Some(BlockHeight::from_u32(100_000)), + blossom: Some(BlockHeight::from_u32(100_000)), + heartwood: Some(BlockHeight::from_u32(100_000)), + canopy: Some(BlockHeight::from_u32(100_000)), + nu5: Some(BlockHeight::from_u32(100_000)), + nu6: None, + #[cfg(zcash_unstable = "zfuture")] + z_future: None, + }; + + /// Constructs a new test environment builder. + 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: (), + initial_chain_state: None, + account_birthday: None, + account_index: None, + } + } +} + +impl Default for TestBuilder<(), ()> { + fn default() -> Self { + Self::new() + } +} + +impl TestBuilder<(), A> { + /// Adds a [`BlockDb`] cache to the test. + pub fn with_block_cache(self, cache: C) -> TestBuilder { + TestBuilder { + rng: self.rng, + network: self.network, + cache, + ds_factory: self.ds_factory, + initial_chain_state: self.initial_chain_state, + account_birthday: self.account_birthday, + account_index: self.account_index, + } + } +} + +impl TestBuilder { + pub fn with_data_store_factory( + self, + ds_factory: DsFactory, + ) -> TestBuilder { + TestBuilder { + rng: self.rng, + network: self.network, + cache: self.cache, + ds_factory, + initial_chain_state: self.initial_chain_state, + account_birthday: self.account_birthday, + account_index: self.account_index, + } + } +} + +impl TestBuilder { + pub fn with_initial_chain_state( + mut self, + chain_state: impl FnOnce(&mut ChaChaRng, &LocalNetwork) -> InitialChainState, + ) -> Self { + assert!(self.initial_chain_state.is_none()); + assert!(self.account_birthday.is_none()); + self.initial_chain_state = Some(chain_state(&mut self.rng, &self.network)); + self + } + + 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( + ChainState::empty( + self.network + .activation_height(NetworkUpgrade::Sapling) + .unwrap() + - 1, + prev_hash, + ), + None, + )); + self + } + + pub fn with_account_having_current_birthday(mut self) -> Self { + assert!(self.account_birthday.is_none()); + assert!(self.initial_chain_state.is_some()); + self.account_birthday = Some(AccountBirthday::from_parts( + self.initial_chain_state + .as_ref() + .unwrap() + .chain_state + .clone(), + None, + )); + self + } + + /// Sets the [`account_index`] field for the test account + /// + /// Call either [`with_account_from_sapling_activation`] or [`with_account_having_current_birthday`] before calling this method. + pub fn set_account_index(mut self, index: zip32::AccountId) -> Self { + assert!(self.account_index.is_none()); + self.account_index = Some(index); + self + } +} + +impl TestBuilder { + /// Builds the state for this test. + pub fn build(self) -> TestState { + let mut cached_blocks = BTreeMap::new(); + let mut wallet_data = self.ds_factory.new_data_store(self.network).unwrap(); + + if let Some(initial_state) = &self.initial_chain_state { + wallet_data + .put_sapling_subtree_roots(0, &initial_state.prior_sapling_roots) + .unwrap(); + wallet_data + .with_sapling_tree_mut(|t| { + t.insert_frontier( + initial_state.chain_state.final_sapling_tree().clone(), + Retention::Checkpoint { + id: initial_state.chain_state.block_height(), + marking: Marking::Reference, + }, + ) + }) + .unwrap(); + + #[cfg(feature = "orchard")] + { + wallet_data + .put_orchard_subtree_roots(0, &initial_state.prior_orchard_roots) + .unwrap(); + wallet_data + .with_orchard_tree_mut(|t| { + t.insert_frontier( + initial_state.chain_state.final_orchard_tree().clone(), + Retention::Checkpoint { + id: initial_state.chain_state.block_height(), + marking: Marking::Reference, + }, + ) + }) + .unwrap(); + } + + let final_sapling_tree_size = + initial_state.chain_state.final_sapling_tree().tree_size() as u32; + let _final_orchard_tree_size = 0; + #[cfg(feature = "orchard")] + let _final_orchard_tree_size = + initial_state.chain_state.final_orchard_tree().tree_size() as u32; + + cached_blocks.insert( + initial_state.chain_state.block_height(), + CachedBlock { + chain_state: initial_state.chain_state.clone(), + sapling_end_size: final_sapling_tree_size, + orchard_end_size: _final_orchard_tree_size, + }, + ); + }; + + let test_account = self.account_birthday.map(|birthday| { + let seed = Secret::new(vec![0u8; 32]); + let (account, usk) = match self.account_index { + Some(index) => wallet_data + .import_account_hd(&seed, index, &birthday) + .unwrap(), + None => { + let result = wallet_data.create_account(&seed, &birthday).unwrap(); + ( + wallet_data.get_account(result.0).unwrap().unwrap(), + result.1, + ) + } + }; + ( + seed, + TestAccount { + account, + usk, + birthday, + }, + ) + }); + + TestState { + cache: self.cache, + cached_blocks, + latest_block_height: self + .initial_chain_state + .map(|s| s.chain_state.block_height()), + wallet_data, + network: self.network, + test_account, + rng: self.rng, + } + } +} + +/// Trait used by tests that require a full viewing key. +pub trait TestFvk { + type Nullifier: Copy; + + fn sapling_ovk(&self) -> Option; + + #[cfg(feature = "orchard")] + fn orchard_ovk(&self, scope: zip32::Scope) -> Option; + + fn add_spend( + &self, + ctx: &mut CompactTx, + nf: Self::Nullifier, + rng: &mut R, + ); + + #[allow(clippy::too_many_arguments)] + fn add_output( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + req: AddressType, + value: NonNegativeAmount, + initial_sapling_tree_size: u32, + // we don't require an initial Orchard tree size because we don't need it to compute + // the nullifier. + rng: &mut R, + ) -> Self::Nullifier; + + #[allow(clippy::too_many_arguments)] + fn add_logical_action( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + nf: Self::Nullifier, + req: AddressType, + value: NonNegativeAmount, + initial_sapling_tree_size: u32, + // we don't require an initial Orchard tree size because we don't need it to compute + // the nullifier. + rng: &mut R, + ) -> Self::Nullifier; +} + +impl<'a, A: TestFvk> TestFvk for &'a A { + type Nullifier = A::Nullifier; + + fn sapling_ovk(&self) -> Option { + (*self).sapling_ovk() + } + + #[cfg(feature = "orchard")] + fn orchard_ovk(&self, scope: zip32::Scope) -> Option { + (*self).orchard_ovk(scope) + } + + fn add_spend( + &self, + ctx: &mut CompactTx, + nf: Self::Nullifier, + rng: &mut R, + ) { + (*self).add_spend(ctx, nf, rng) + } + + fn add_output( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + req: AddressType, + value: Zatoshis, + initial_sapling_tree_size: u32, + // we don't require an initial Orchard tree size because we don't need it to compute + // the nullifier. + rng: &mut R, + ) -> Self::Nullifier { + (*self).add_output( + ctx, + params, + height, + req, + value, + initial_sapling_tree_size, + rng, + ) + } + + fn add_logical_action( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + nf: Self::Nullifier, + req: AddressType, + value: Zatoshis, + initial_sapling_tree_size: u32, + // we don't require an initial Orchard tree size because we don't need it to compute + // the nullifier. + rng: &mut R, + ) -> Self::Nullifier { + (*self).add_logical_action( + ctx, + params, + height, + nf, + req, + value, + initial_sapling_tree_size, + rng, + ) + } +} + +impl TestFvk for DiversifiableFullViewingKey { + type Nullifier = ::sapling::Nullifier; + + fn sapling_ovk(&self) -> Option { + Some(self.fvk().ovk) + } + + #[cfg(feature = "orchard")] + fn orchard_ovk(&self, _: zip32::Scope) -> Option { + None + } + + fn add_spend( + &self, + ctx: &mut CompactTx, + nf: Self::Nullifier, + _: &mut R, + ) { + let cspend = CompactSaplingSpend { nf: nf.to_vec() }; + ctx.spends.push(cspend); + } + + fn add_output( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + req: AddressType, + value: NonNegativeAmount, + initial_sapling_tree_size: u32, + rng: &mut R, + ) -> Self::Nullifier { + let recipient = match req { + AddressType::DefaultExternal => self.default_address().1, + AddressType::DiversifiedExternal(idx) => self.find_address(idx).unwrap().1, + AddressType::Internal => self.change_address().1, + }; + + let position = initial_sapling_tree_size + ctx.outputs.len() as u32; + + let (cout, note) = + compact_sapling_output(params, height, recipient, value, self.sapling_ovk(), rng); + ctx.outputs.push(cout); + + note.nf(&self.fvk().vk.nk, position as u64) + } + + #[allow(clippy::too_many_arguments)] + fn add_logical_action( + &self, + ctx: &mut CompactTx, + params: &P, + height: BlockHeight, + nf: Self::Nullifier, + req: AddressType, + value: NonNegativeAmount, + initial_sapling_tree_size: u32, + rng: &mut R, + ) -> Self::Nullifier { + self.add_spend(ctx, nf, rng); + self.add_output( + ctx, + params, + height, + req, + value, + initial_sapling_tree_size, + rng, + ) + } +} + +#[cfg(feature = "orchard")] +impl TestFvk for orchard::keys::FullViewingKey { + type Nullifier = orchard::note::Nullifier; + + fn sapling_ovk(&self) -> Option { + None + } + + fn orchard_ovk(&self, scope: zip32::Scope) -> Option { + Some(self.to_ovk(scope)) + } + + fn add_spend( + &self, + ctx: &mut CompactTx, + revealed_spent_note_nullifier: Self::Nullifier, + rng: &mut R, + ) { + // Generate a dummy recipient. + let recipient = loop { + let mut bytes = [0; 32]; + rng.fill_bytes(&mut bytes); + let sk = orchard::keys::SpendingKey::from_bytes(bytes); + if sk.is_some().into() { + break orchard::keys::FullViewingKey::from(&sk.unwrap()) + .address_at(0u32, zip32::Scope::External); + } + }; + + let (cact, _) = compact_orchard_action( + revealed_spent_note_nullifier, + recipient, + NonNegativeAmount::ZERO, + self.orchard_ovk(zip32::Scope::Internal), + rng, + ); + ctx.actions.push(cact); + } + + fn add_output( + &self, + ctx: &mut CompactTx, + _: &P, + _: BlockHeight, + req: AddressType, + value: NonNegativeAmount, + _: u32, // the position is not required for computing the Orchard nullifier + mut rng: &mut R, + ) -> Self::Nullifier { + // Generate a dummy nullifier for the spend + let revealed_spent_note_nullifier = + orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr()) + .unwrap(); + + let (j, scope) = match req { + AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External), + AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External), + AddressType::Internal => (0u32.into(), zip32::Scope::Internal), + }; + + let (cact, note) = compact_orchard_action( + revealed_spent_note_nullifier, + self.address_at(j, scope), + value, + self.orchard_ovk(scope), + rng, + ); + ctx.actions.push(cact); + + note.nullifier(self) + } + + // Override so we can merge the spend and output into a single action. + fn add_logical_action( + &self, + ctx: &mut CompactTx, + _: &P, + _: BlockHeight, + revealed_spent_note_nullifier: Self::Nullifier, + address_type: AddressType, + value: NonNegativeAmount, + _: u32, // the position is not required for computing the Orchard nullifier + rng: &mut R, + ) -> Self::Nullifier { + let (j, scope) = match address_type { + AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External), + AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External), + AddressType::Internal => (0u32.into(), zip32::Scope::Internal), + }; + + let (cact, note) = compact_orchard_action( + revealed_spent_note_nullifier, + self.address_at(j, scope), + value, + self.orchard_ovk(scope), + rng, + ); + ctx.actions.push(cact); + + // Return the nullifier of the newly created output note + note.nullifier(self) + } +} + +#[derive(Clone, Copy)] +pub enum AddressType { + DefaultExternal, + #[allow(dead_code)] + DiversifiedExternal(DiversifierIndex), + Internal, +} + +/// Creates a `CompactSaplingOutput` at the given height paying the given recipient. +/// +/// Returns the `CompactSaplingOutput` and the new note. +fn compact_sapling_output( + params: &P, + height: BlockHeight, + recipient: sapling::PaymentAddress, + value: NonNegativeAmount, + ovk: Option, + rng: &mut R, +) -> (CompactSaplingOutput, sapling::Note) { + let rseed = generate_random_rseed(zip212_enforcement(params, height), rng); + let note = ::sapling::Note::from_parts( + recipient, + sapling::value::NoteValue::from_raw(value.into_u64()), + rseed, + ); + let encryptor = sapling_note_encryption(ovk, note.clone(), *MemoBytes::empty().as_array(), rng); + let cmu = note.cmu().to_bytes().to_vec(); + let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec(); + let enc_ciphertext = encryptor.encrypt_note_plaintext(); + + ( + CompactSaplingOutput { + cmu, + ephemeral_key, + ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), + }, + note, + ) +} + +/// Creates a `CompactOrchardAction` at the given height paying the given recipient. +/// +/// Returns the `CompactOrchardAction` and the new note. +#[cfg(feature = "orchard")] +fn compact_orchard_action( + nf_old: orchard::note::Nullifier, + recipient: orchard::Address, + value: NonNegativeAmount, + ovk: Option, + rng: &mut R, +) -> (CompactOrchardAction, orchard::Note) { + use zcash_note_encryption::ShieldedOutput; + + let (compact_action, note) = orchard::note_encryption::testing::fake_compact_action( + rng, + nf_old, + recipient, + orchard::value::NoteValue::from_raw(value.into_u64()), + ovk, + ); + + ( + CompactOrchardAction { + nullifier: compact_action.nullifier().to_bytes().to_vec(), + cmx: compact_action.cmx().to_bytes().to_vec(), + ephemeral_key: compact_action.ephemeral_key().0.to_vec(), + ciphertext: compact_action.enc_ciphertext().as_ref()[..52].to_vec(), + }, + note, + ) +} + +/// Creates a fake `CompactTx` with a random transaction ID and no spends or outputs. +fn fake_compact_tx(rng: &mut R) -> CompactTx { + let mut ctx = CompactTx::default(); + let mut txid = vec![0; 32]; + rng.fill_bytes(&mut txid); + ctx.hash = txid; + + ctx +} + +#[derive(Clone)] +pub struct FakeCompactOutput { + fvk: Fvk, + address_type: AddressType, + value: NonNegativeAmount, +} + +impl FakeCompactOutput { + pub fn new(fvk: Fvk, address_type: AddressType, value: NonNegativeAmount) -> Self { + Self { + fvk, + address_type, + value, + } + } +} + +/// Create a fake CompactBlock at the given height, containing the specified fake compact outputs. +/// +/// Returns the newly created compact block, along with the nullifier for each note created in that +/// block. +#[allow(clippy::too_many_arguments)] +fn fake_compact_block( + params: &P, + height: BlockHeight, + prev_hash: BlockHash, + outputs: &[FakeCompactOutput], + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + mut rng: impl RngCore + CryptoRng, +) -> (CompactBlock, Vec) { + // Create a fake CompactBlock containing the note + let mut ctx = fake_compact_tx(&mut rng); + let mut nfs = vec![]; + for output in outputs { + let nf = output.fvk.add_output( + &mut ctx, + params, + height, + output.address_type, + output.value, + initial_sapling_tree_size, + &mut rng, + ); + nfs.push(nf); + } + + let cb = fake_compact_block_from_compact_tx( + ctx, + height, + prev_hash, + initial_sapling_tree_size, + initial_orchard_tree_size, + rng, + ); + (cb, nfs) +} + +/// Create a fake CompactBlock at the given height containing only the given transaction. +fn fake_compact_block_from_tx( + height: BlockHeight, + prev_hash: BlockHash, + tx_index: usize, + tx: &Transaction, + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + rng: impl RngCore, +) -> CompactBlock { + // Create a fake CompactTx containing the transaction. + let mut ctx = CompactTx { + index: tx_index as u64, + hash: tx.txid().as_ref().to_vec(), + ..Default::default() + }; + + if let Some(bundle) = tx.sapling_bundle() { + for spend in bundle.shielded_spends() { + ctx.spends.push(spend.into()); + } + for output in bundle.shielded_outputs() { + ctx.outputs.push(output.into()); + } + } + + #[cfg(feature = "orchard")] + if let Some(bundle) = tx.orchard_bundle() { + for action in bundle.actions() { + ctx.actions.push(action.into()); + } + } + + fake_compact_block_from_compact_tx( + ctx, + height, + prev_hash, + initial_sapling_tree_size, + initial_orchard_tree_size, + rng, + ) +} + +/// Create a fake CompactBlock at the given height, spending a single note from the +/// given address. +#[allow(clippy::too_many_arguments)] +fn fake_compact_block_spending( + params: &P, + height: BlockHeight, + prev_hash: BlockHash, + (nf, in_value): (Fvk::Nullifier, NonNegativeAmount), + fvk: &Fvk, + to: Address, + value: NonNegativeAmount, + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + mut rng: impl RngCore + CryptoRng, +) -> CompactBlock { + let mut ctx = fake_compact_tx(&mut rng); + + // Create a fake spend and a fake Note for the change + fvk.add_logical_action( + &mut ctx, + params, + height, + nf, + AddressType::Internal, + (in_value - value).unwrap(), + initial_sapling_tree_size, + &mut rng, + ); + + // Create a fake Note for the payment + match to { + Address::Sapling(recipient) => ctx.outputs.push( + compact_sapling_output( + params, + height, + recipient, + value, + fvk.sapling_ovk(), + &mut rng, + ) + .0, + ), + Address::Transparent(_) | Address::Tex(_) => { + panic!("transparent addresses not supported in compact blocks") + } + Address::Unified(ua) => { + // This is annoying to implement, because the protocol-aware UA type has no + // concept of ZIP 316 preference order. + let mut done = false; + + #[cfg(feature = "orchard")] + if let Some(recipient) = ua.orchard() { + // Generate a dummy nullifier + let nullifier = + orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr()) + .unwrap(); + + ctx.actions.push( + compact_orchard_action( + nullifier, + *recipient, + value, + fvk.orchard_ovk(zip32::Scope::External), + &mut rng, + ) + .0, + ); + done = true; + } + + if !done { + if let Some(recipient) = ua.sapling() { + ctx.outputs.push( + compact_sapling_output( + params, + height, + *recipient, + value, + fvk.sapling_ovk(), + &mut rng, + ) + .0, + ); + done = true; + } + } + if !done { + panic!("No supported shielded receiver to send funds to"); + } + } + } + + fake_compact_block_from_compact_tx( + ctx, + height, + prev_hash, + initial_sapling_tree_size, + initial_orchard_tree_size, + rng, + ) +} + +fn fake_compact_block_from_compact_tx( + ctx: CompactTx, + height: BlockHeight, + prev_hash: BlockHash, + initial_sapling_tree_size: u32, + initial_orchard_tree_size: u32, + mut rng: impl RngCore, +) -> CompactBlock { + let mut cb = CompactBlock { + hash: { + let mut hash = vec![0; 32]; + rng.fill_bytes(&mut hash); + hash + }, + height: height.into(), + ..Default::default() + }; + cb.prev_hash.extend_from_slice(&prev_hash.0); + cb.vtx.push(ctx); + cb.chain_metadata = Some(compact_formats::ChainMetadata { + sapling_commitment_tree_size: initial_sapling_tree_size + + cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::(), + orchard_commitment_tree_size: initial_orchard_tree_size + + cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum::(), + }); + cb +} + +/// Trait used by tests that require a block cache. +pub trait TestCache { + type BlockSource: BlockSource; + type InsertResult; + + /// Exposes the block cache as a [`BlockSource`]. + fn block_source(&self) -> &Self::BlockSource; + + /// Inserts a CompactBlock into the cache DB. + fn insert(&self, cb: &CompactBlock) -> Self::InsertResult; +} + +pub struct NoteCommitments { + sapling: Vec, + #[cfg(feature = "orchard")] + orchard: Vec, +} + +impl NoteCommitments { + pub fn from_compact_block(cb: &CompactBlock) -> Self { + NoteCommitments { + sapling: cb + .vtx + .iter() + .flat_map(|tx| { + tx.outputs + .iter() + .map(|out| sapling::Node::from_cmu(&out.cmu().unwrap())) + }) + .collect(), + #[cfg(feature = "orchard")] + orchard: cb + .vtx + .iter() + .flat_map(|tx| { + tx.actions + .iter() + .map(|act| MerkleHashOrchard::from_cmx(&act.cmx().unwrap())) + }) + .collect(), + } + } + + #[allow(dead_code)] + pub fn sapling(&self) -> &[sapling::Node] { + self.sapling.as_ref() + } + + #[cfg(feature = "orchard")] + pub fn orchard(&self) -> &[MerkleHashOrchard] { + self.orchard.as_ref() + } +} + pub struct MockWalletDb { pub network: Network, pub sapling_tree: ShardTree< @@ -503,7 +2391,10 @@ impl WalletCommitmentTrees for MockWalletDb { ) -> Result<(), ShardTreeError> { 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); + let root_addr = incrementalmerkletree::Address::from_parts( + SAPLING_SHARD_HEIGHT.into(), + start_index + i, + ); t.insert(root_addr, *root.root_hash())?; } Ok::<_, ShardTreeError>(()) @@ -539,7 +2430,10 @@ impl WalletCommitmentTrees for MockWalletDb { ) -> Result<(), ShardTreeError> { 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); + let root_addr = incrementalmerkletree::Address::from_parts( + ORCHARD_SHARD_HEIGHT.into(), + start_index + i, + ); t.insert(root_addr, *root.root_hash())?; } Ok::<_, ShardTreeError>(()) diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 8d092686a5..9091fe4a3a 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -1698,22 +1698,21 @@ 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::{db::TestDbFactory, 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, }; @@ -1996,8 +1995,9 @@ 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; use crate::testing::FsBlockCache; diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 7e8ff542c5..2aaafb6766 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -1,93 +1,21 @@ -use std::fmt; -use std::num::NonZeroU32; -use std::{collections::BTreeMap, convert::Infallible}; - -use group::ff::Field; -use incrementalmerkletree::{Marking, Retention}; -use nonempty::NonEmpty; use prost::Message; -use rand_chacha::ChaChaRng; -use rand_core::{CryptoRng, RngCore, SeedableRng}; + use rusqlite::params; -use secrecy::{Secret, SecretVec}; -use shardtree::error::ShardTreeError; -use subtle::ConditionallySelectable; use tempfile::NamedTempFile; #[cfg(feature = "unstable")] use {std::fs::File, tempfile::TempDir}; -use sapling::{ - note_encryption::{sapling_note_encryption, SaplingDomain}, - util::generate_random_rseed, - zip32::DiversifiableFullViewingKey, - Note, Nullifier, -}; -use zcash_client_backend::data_api::testing::TransactionSummary; -use zcash_client_backend::data_api::{Account, InputSource}; +use zcash_client_backend::data_api::testing::{NoteCommitments, TestCache}; + #[allow(deprecated)] -use zcash_client_backend::{ - address::Address, - data_api::{ - self, - chain::{scan_cached_blocks, BlockSource, CommitmentTreeRoot, ScanSummary}, - wallet::{ - create_proposed_transactions, create_spend_to_address, - input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector}, - propose_standard_transfer_to_address, propose_transfer, spend, - }, - AccountBalance, AccountBirthday, WalletCommitmentTrees, WalletRead, WalletSummary, - WalletWrite, - }, - keys::UnifiedSpendingKey, - proposal::Proposal, - proto::compact_formats::{ - self as compact, CompactBlock, CompactSaplingOutput, CompactSaplingSpend, CompactTx, - }, - proto::proposal, - wallet::OvkPolicy, - zip321, -}; -use zcash_client_backend::{ - data_api::chain::ChainState, - fees::{standard, DustOutputPolicy}, - ShieldedProtocol, -}; -use zcash_note_encryption::Domain; -use zcash_primitives::{ - block::BlockHash, - consensus::{self, BlockHeight, NetworkUpgrade, Parameters}, - memo::{Memo, MemoBytes}, - transaction::{ - components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, - fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule}, - Transaction, TxId, - }, - zip32::DiversifierIndex, -}; -use zcash_protocol::local_consensus::LocalNetwork; -use zcash_protocol::value::Zatoshis; +use zcash_client_backend::proto::compact_formats::CompactBlock; use crate::chain::init::init_cache_database; -use crate::{wallet::sapling::tests::test_prover, ReceivedNoteId}; use super::BlockDb; -#[cfg(feature = "orchard")] -use { - group::ff::PrimeField, orchard::tree::MerkleHashOrchard, pasta_curves::pallas, - zcash_client_backend::proto::compact_formats::CompactOrchardAction, -}; - -#[cfg(feature = "transparent-inputs")] -use { - zcash_client_backend::data_api::wallet::{ - input_selection::ShieldingSelector, propose_shielding, shield_transparent_funds, - }, - zcash_primitives::legacy::TransparentAddress, -}; - #[cfg(feature = "unstable")] use crate::{ chain::{init::init_blockmeta_db, BlockMeta}, @@ -97,1787 +25,6 @@ use crate::{ pub(crate) mod db; pub(crate) mod pool; -pub(crate) struct InitialChainState { - pub(crate) chain_state: ChainState, - pub(crate) prior_sapling_roots: Vec>, - #[cfg(feature = "orchard")] - pub(crate) prior_orchard_roots: Vec>, -} - -pub(crate) trait DataStoreFactory { - type Error: core::fmt::Debug; - type AccountId: ConditionallySelectable + Default + Send + 'static; - type DataStore: InputSource - + WalletRead - + WalletWrite - + WalletCommitmentTrees; - - fn new_data_store(&self, network: LocalNetwork) -> Result; -} - -/// A builder for a `zcash_client_sqlite` test. -pub(crate) struct TestBuilder { - rng: ChaChaRng, - network: LocalNetwork, - cache: Cache, - ds_factory: DataStoreFactory, - initial_chain_state: Option, - account_birthday: Option, - account_index: Option, -} - -impl TestBuilder<(), ()> { - pub const DEFAULT_NETWORK: LocalNetwork = LocalNetwork { - overwinter: Some(BlockHeight::from_u32(1)), - sapling: Some(BlockHeight::from_u32(100_000)), - blossom: Some(BlockHeight::from_u32(100_000)), - heartwood: Some(BlockHeight::from_u32(100_000)), - canopy: Some(BlockHeight::from_u32(100_000)), - nu5: Some(BlockHeight::from_u32(100_000)), - nu6: None, - #[cfg(zcash_unstable = "zfuture")] - z_future: None, - }; - - /// Constructs a new test environment builder. - pub(crate) 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: (), - initial_chain_state: None, - account_birthday: None, - account_index: None, - } - } -} - -impl TestBuilder<(), A> { - /// Adds a [`BlockDb`] cache to the test. - pub(crate) fn with_block_cache(self, cache: C) -> TestBuilder { - TestBuilder { - rng: self.rng, - network: self.network, - cache, - ds_factory: self.ds_factory, - initial_chain_state: self.initial_chain_state, - account_birthday: self.account_birthday, - account_index: self.account_index, - } - } -} - -impl TestBuilder { - pub(crate) fn with_data_store_factory( - self, - ds_factory: DsFactory, - ) -> TestBuilder { - TestBuilder { - rng: self.rng, - network: self.network, - cache: self.cache, - ds_factory, - initial_chain_state: self.initial_chain_state, - account_birthday: self.account_birthday, - account_index: self.account_index, - } - } -} - -impl TestBuilder { - pub(crate) fn with_initial_chain_state( - mut self, - chain_state: impl FnOnce(&mut ChaChaRng, &LocalNetwork) -> InitialChainState, - ) -> Self { - assert!(self.initial_chain_state.is_none()); - assert!(self.account_birthday.is_none()); - self.initial_chain_state = Some(chain_state(&mut self.rng, &self.network)); - self - } - - pub(crate) 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( - ChainState::empty( - self.network - .activation_height(NetworkUpgrade::Sapling) - .unwrap() - - 1, - prev_hash, - ), - None, - )); - self - } - - pub(crate) fn with_account_having_current_birthday(mut self) -> Self { - assert!(self.account_birthday.is_none()); - assert!(self.initial_chain_state.is_some()); - self.account_birthday = Some(AccountBirthday::from_parts( - self.initial_chain_state - .as_ref() - .unwrap() - .chain_state - .clone(), - None, - )); - self - } - - /// Sets the [`account_index`] field for the test account - /// - /// Call either [`with_account_from_sapling_activation`] or [`with_account_having_current_birthday`] before calling this method. - pub(crate) fn set_account_index(mut self, index: zip32::AccountId) -> Self { - assert!(self.account_index.is_none()); - self.account_index = Some(index); - self - } -} - -impl TestBuilder { - /// Builds the state for this test. - pub(crate) fn build(self) -> TestState { - let mut cached_blocks = BTreeMap::new(); - let mut wallet_data = self.ds_factory.new_data_store(self.network).unwrap(); - - if let Some(initial_state) = &self.initial_chain_state { - wallet_data - .put_sapling_subtree_roots(0, &initial_state.prior_sapling_roots) - .unwrap(); - wallet_data - .with_sapling_tree_mut(|t| { - t.insert_frontier( - initial_state.chain_state.final_sapling_tree().clone(), - Retention::Checkpoint { - id: initial_state.chain_state.block_height(), - marking: Marking::Reference, - }, - ) - }) - .unwrap(); - - #[cfg(feature = "orchard")] - { - wallet_data - .put_orchard_subtree_roots(0, &initial_state.prior_orchard_roots) - .unwrap(); - wallet_data - .with_orchard_tree_mut(|t| { - t.insert_frontier( - initial_state.chain_state.final_orchard_tree().clone(), - Retention::Checkpoint { - id: initial_state.chain_state.block_height(), - marking: Marking::Reference, - }, - ) - }) - .unwrap(); - } - - let final_sapling_tree_size = - initial_state.chain_state.final_sapling_tree().tree_size() as u32; - let _final_orchard_tree_size = 0; - #[cfg(feature = "orchard")] - let _final_orchard_tree_size = - initial_state.chain_state.final_orchard_tree().tree_size() as u32; - - cached_blocks.insert( - initial_state.chain_state.block_height(), - CachedBlock { - chain_state: initial_state.chain_state.clone(), - sapling_end_size: final_sapling_tree_size, - orchard_end_size: _final_orchard_tree_size, - }, - ); - }; - - let test_account = self.account_birthday.map(|birthday| { - let seed = Secret::new(vec![0u8; 32]); - let (account, usk) = match self.account_index { - Some(index) => wallet_data - .import_account_hd(&seed, index, &birthday) - .unwrap(), - None => { - let result = wallet_data.create_account(&seed, &birthday).unwrap(); - ( - wallet_data.get_account(result.0).unwrap().unwrap(), - result.1, - ) - } - }; - ( - seed, - TestAccount { - account, - usk, - birthday, - }, - ) - }); - - TestState { - cache: self.cache, - cached_blocks, - latest_block_height: self - .initial_chain_state - .map(|s| s.chain_state.block_height()), - wallet_data, - network: self.network, - test_account, - rng: self.rng, - } - } -} - -#[derive(Clone, Debug)] -pub(crate) struct CachedBlock { - chain_state: ChainState, - sapling_end_size: u32, - orchard_end_size: u32, -} - -impl CachedBlock { - fn none(sapling_activation_height: BlockHeight) -> Self { - Self { - chain_state: ChainState::empty(sapling_activation_height, BlockHash([0; 32])), - sapling_end_size: 0, - orchard_end_size: 0, - } - } - - 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, - sapling_end_size - ); - #[cfg(feature = "orchard")] - assert_eq!( - chain_state.final_orchard_tree().tree_size() as u32, - orchard_end_size - ); - - Self { - chain_state, - sapling_end_size, - orchard_end_size, - } - } - - fn roll_forward(&self, cb: &CompactBlock) -> Self { - assert_eq!(self.chain_state.block_height() + 1, cb.height()); - - let sapling_final_tree = cb.vtx.iter().flat_map(|tx| tx.outputs.iter()).fold( - self.chain_state.final_sapling_tree().clone(), - |mut acc, c_out| { - acc.append(sapling::Node::from_cmu(&c_out.cmu().unwrap())); - acc - }, - ); - let sapling_end_size = sapling_final_tree.tree_size() as u32; - - #[cfg(feature = "orchard")] - let orchard_final_tree = cb.vtx.iter().flat_map(|tx| tx.actions.iter()).fold( - self.chain_state.final_orchard_tree().clone(), - |mut acc, c_act| { - acc.append(MerkleHashOrchard::from_cmx(&c_act.cmx().unwrap())); - acc - }, - ); - #[cfg(feature = "orchard")] - let orchard_end_size = orchard_final_tree.tree_size() as u32; - #[cfg(not(feature = "orchard"))] - let orchard_end_size = cb.vtx.iter().fold(self.orchard_end_size, |sz, tx| { - sz + (tx.actions.len() as u32) - }); - - Self { - chain_state: ChainState::new( - cb.height(), - cb.hash(), - sapling_final_tree, - #[cfg(feature = "orchard")] - orchard_final_tree, - ), - sapling_end_size, - orchard_end_size, - } - } - - fn height(&self) -> BlockHeight { - self.chain_state.block_height() - } -} - -#[derive(Clone)] -pub(crate) struct TestAccount { - account: A, - usk: UnifiedSpendingKey, - birthday: AccountBirthday, -} - -impl TestAccount { - pub(crate) fn account(&self) -> &A { - &self.account - } - - pub(crate) fn usk(&self) -> &UnifiedSpendingKey { - &self.usk - } - - pub(crate) fn birthday(&self) -> &AccountBirthday { - &self.birthday - } -} - -impl Account for TestAccount { - type AccountId = A::AccountId; - - fn id(&self) -> Self::AccountId { - self.account.id() - } - - fn source(&self) -> data_api::AccountSource { - self.account.source() - } - - fn ufvk(&self) -> Option<&zcash_keys::keys::UnifiedFullViewingKey> { - self.account.ufvk() - } - - fn uivk(&self) -> zcash_keys::keys::UnifiedIncomingViewingKey { - self.account.uivk() - } -} - -pub(crate) trait Reset: WalletRead + Sized { - type Handle; - - fn reset(st: &mut TestState) -> Self::Handle; -} - -/// The state for a `zcash_client_sqlite` test. -pub(crate) struct TestState { - cache: Cache, - cached_blocks: BTreeMap, - latest_block_height: Option, - wallet_data: DataStore, - network: Network, - test_account: Option<(SecretVec, TestAccount)>, - rng: ChaChaRng, -} - -impl TestState { - /// Exposes an immutable reference to the test's `DataStore`. - pub(crate) fn wallet(&self) -> &DataStore { - &self.wallet_data - } - - /// Exposes a mutable reference to the test's `DataStore`. - pub(crate) fn wallet_mut(&mut self) -> &mut DataStore { - &mut self.wallet_data - } - - /// Exposes the test framework's source of randomness. - pub(crate) fn rng_mut(&mut self) -> &mut ChaChaRng { - &mut self.rng - } - - /// Exposes the network in use. - pub(crate) fn network(&self) -> &Network { - &self.network - } -} - -impl - TestState -{ - /// Convenience method for obtaining the Sapling activation height for the network under test. - pub(crate) fn sapling_activation_height(&self) -> BlockHeight { - self.network - .activation_height(NetworkUpgrade::Sapling) - .expect("Sapling activation height must be known.") - } - - /// Convenience method for obtaining the NU5 activation height for the network under test. - #[allow(dead_code)] - pub(crate) fn nu5_activation_height(&self) -> BlockHeight { - self.network - .activation_height(NetworkUpgrade::Nu5) - .expect("NU5 activation height must be known.") - } - - /// Exposes the test seed, if enabled via [`TestBuilder::with_test_account`]. - pub(crate) fn test_seed(&self) -> Option<&SecretVec> { - self.test_account.as_ref().map(|(seed, _)| seed) - } -} - -impl TestState -where - Network: consensus::Parameters, - DataStore: WalletRead, -{ - /// Exposes the test account, if enabled via [`TestBuilder::with_test_account`]. - pub(crate) fn test_account(&self) -> Option<&TestAccount<::Account>> { - self.test_account.as_ref().map(|(_, acct)| acct) - } - - /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. - pub(crate) fn test_account_sapling(&self) -> Option<&DiversifiableFullViewingKey> { - let (_, acct) = self.test_account.as_ref()?; - let ufvk = acct.ufvk()?; - ufvk.sapling() - } - - /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. - #[cfg(feature = "orchard")] - pub(crate) fn test_account_orchard(&self) -> Option<&orchard::keys::FullViewingKey> { - let (_, acct) = self.test_account.as_ref()?; - let ufvk = acct.ufvk()?; - ufvk.orchard() - } -} - -impl TestState -where - Network: consensus::Parameters, - DataStore: WalletWrite, - ::Error: fmt::Debug, -{ - /// Exposes an immutable reference to the test's [`BlockSource`]. - #[cfg(feature = "unstable")] - pub(crate) fn cache(&self) -> &Cache::BlockSource { - self.cache.block_source() - } - - pub(crate) fn latest_cached_block(&self) -> Option<&CachedBlock> { - self.latest_block_height - .as_ref() - .and_then(|h| self.cached_blocks.get(h)) - } - - fn latest_cached_block_below_height(&self, height: BlockHeight) -> Option<&CachedBlock> { - self.cached_blocks.range(..height).last().map(|(_, b)| b) - } - - fn cache_block( - &mut self, - prev_block: &CachedBlock, - compact_block: CompactBlock, - ) -> Cache::InsertResult { - self.cached_blocks.insert( - compact_block.height(), - prev_block.roll_forward(&compact_block), - ); - self.cache.insert(&compact_block) - } - /// Creates a fake block at the expected next height containing a single output of the - /// given value, and inserts it into the cache. - pub(crate) fn generate_next_block( - &mut self, - fvk: &Fvk, - address_type: AddressType, - value: NonNegativeAmount, - ) -> (BlockHeight, Cache::InsertResult, Fvk::Nullifier) { - let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1); - let prior_cached_block = self.latest_cached_block().unwrap_or(&pre_activation_block); - let height = prior_cached_block.height() + 1; - - let (res, nfs) = self.generate_block_at( - height, - prior_cached_block.chain_state.block_hash(), - &[FakeCompactOutput::new(fvk, address_type, value)], - prior_cached_block.sapling_end_size, - prior_cached_block.orchard_end_size, - false, - ); - - (height, res, nfs[0]) - } - - /// Creates a fake block at the expected next height containing multiple outputs - /// and inserts it into the cache. - #[allow(dead_code)] - pub(crate) fn generate_next_block_multi( - &mut self, - outputs: &[FakeCompactOutput], - ) -> (BlockHeight, Cache::InsertResult, Vec) { - let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1); - let prior_cached_block = self.latest_cached_block().unwrap_or(&pre_activation_block); - let height = prior_cached_block.height() + 1; - - let (res, nfs) = self.generate_block_at( - height, - prior_cached_block.chain_state.block_hash(), - outputs, - prior_cached_block.sapling_end_size, - prior_cached_block.orchard_end_size, - false, - ); - - (height, res, nfs) - } - - /// Adds an empty block to the cache, advancing the simulated chain height. - #[allow(dead_code)] // used only for tests that are flagged off by default - pub(crate) fn generate_empty_block(&mut self) -> (BlockHeight, Cache::InsertResult) { - let new_hash = { - let mut hash = vec![0; 32]; - self.rng.fill_bytes(&mut hash); - hash - }; - - let pre_activation_block = CachedBlock::none(self.sapling_activation_height() - 1); - let prior_cached_block = self - .latest_cached_block() - .unwrap_or(&pre_activation_block) - .clone(); - let new_height = prior_cached_block.height() + 1; - - let mut cb = CompactBlock { - hash: new_hash, - height: new_height.into(), - ..Default::default() - }; - cb.prev_hash - .extend_from_slice(&prior_cached_block.chain_state.block_hash().0); - - cb.chain_metadata = Some(compact::ChainMetadata { - sapling_commitment_tree_size: prior_cached_block.sapling_end_size, - orchard_commitment_tree_size: prior_cached_block.orchard_end_size, - }); - - let res = self.cache_block(&prior_cached_block, cb); - self.latest_block_height = Some(new_height); - - (new_height, res) - } - - /// Creates a fake block with the given height and hash containing the requested outputs, and - /// inserts it into the cache. - /// - /// This generated block will be treated as the latest block, and subsequent calls to - /// [`Self::generate_next_block`] will build on it. - #[allow(clippy::too_many_arguments)] - pub(crate) fn generate_block_at( - &mut self, - height: BlockHeight, - prev_hash: BlockHash, - outputs: &[FakeCompactOutput], - initial_sapling_tree_size: u32, - initial_orchard_tree_size: u32, - allow_broken_hash_chain: bool, - ) -> (Cache::InsertResult, Vec) { - let mut prior_cached_block = self - .latest_cached_block_below_height(height) - .cloned() - .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1)); - assert!(prior_cached_block.chain_state.block_height() < height); - assert!(prior_cached_block.sapling_end_size <= initial_sapling_tree_size); - assert!(prior_cached_block.orchard_end_size <= initial_orchard_tree_size); - - // If the block height has increased or the Sapling and/or Orchard tree sizes have changed, - // we need to generate a new prior cached block that the block to be generated can - // successfully chain from, with the provided tree sizes. - if prior_cached_block.chain_state.block_height() == height - 1 { - if !allow_broken_hash_chain { - assert_eq!(prev_hash, prior_cached_block.chain_state.block_hash()); - } - } else { - let final_sapling_tree = - (prior_cached_block.sapling_end_size..initial_sapling_tree_size).fold( - prior_cached_block.chain_state.final_sapling_tree().clone(), - |mut acc, _| { - acc.append(sapling::Node::from_scalar(bls12_381::Scalar::random( - &mut self.rng, - ))); - acc - }, - ); - - #[cfg(feature = "orchard")] - let final_orchard_tree = - (prior_cached_block.orchard_end_size..initial_orchard_tree_size).fold( - prior_cached_block.chain_state.final_orchard_tree().clone(), - |mut acc, _| { - acc.append(MerkleHashOrchard::random(&mut self.rng)); - acc - }, - ); - - prior_cached_block = CachedBlock::at( - ChainState::new( - height - 1, - prev_hash, - final_sapling_tree, - #[cfg(feature = "orchard")] - final_orchard_tree, - ), - initial_sapling_tree_size, - initial_orchard_tree_size, - ); - - self.cached_blocks - .insert(height - 1, prior_cached_block.clone()); - } - - let (cb, nfs) = fake_compact_block( - &self.network, - height, - prev_hash, - outputs, - initial_sapling_tree_size, - initial_orchard_tree_size, - &mut self.rng, - ); - assert_eq!(cb.height(), height); - - let res = self.cache_block(&prior_cached_block, cb); - self.latest_block_height = Some(height); - - (res, nfs) - } - - /// Creates a fake block at the expected next height spending the given note, and - /// inserts it into the cache. - pub(crate) fn generate_next_block_spending( - &mut self, - fvk: &Fvk, - note: (Fvk::Nullifier, NonNegativeAmount), - to: impl Into
, - value: NonNegativeAmount, - ) -> (BlockHeight, Cache::InsertResult) { - let prior_cached_block = self - .latest_cached_block() - .cloned() - .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1)); - let height = prior_cached_block.height() + 1; - - let cb = fake_compact_block_spending( - &self.network, - height, - prior_cached_block.chain_state.block_hash(), - note, - fvk, - to.into(), - value, - prior_cached_block.sapling_end_size, - prior_cached_block.orchard_end_size, - &mut self.rng, - ); - assert_eq!(cb.height(), height); - - let res = self.cache_block(&prior_cached_block, cb); - self.latest_block_height = Some(height); - - (height, res) - } - - /// Creates a fake block at the expected next height containing only the wallet - /// transaction with the given txid, and inserts it into the cache. - /// - /// This generated block will be treated as the latest block, and subsequent calls to - /// [`Self::generate_next_block`] (or similar) will build on it. - pub(crate) fn generate_next_block_including( - &mut self, - txid: TxId, - ) -> (BlockHeight, Cache::InsertResult) { - let tx = self - .wallet() - .get_transaction(txid) - .unwrap() - .expect("TxId should exist in the wallet"); - - // Index 0 is by definition a coinbase transaction, and the wallet doesn't - // construct coinbase transactions. So we pretend here that the block has a - // coinbase transaction that does not have shielded coinbase outputs. - self.generate_next_block_from_tx(1, &tx) - } - - /// Creates a fake block at the expected next height containing only the given - /// transaction, and inserts it into the cache. - /// - /// This generated block will be treated as the latest block, and subsequent calls to - /// [`Self::generate_next_block`] will build on it. - pub(crate) fn generate_next_block_from_tx( - &mut self, - tx_index: usize, - tx: &Transaction, - ) -> (BlockHeight, Cache::InsertResult) { - let prior_cached_block = self - .latest_cached_block() - .cloned() - .unwrap_or_else(|| CachedBlock::none(self.sapling_activation_height() - 1)); - let height = prior_cached_block.height() + 1; - - let cb = fake_compact_block_from_tx( - height, - prior_cached_block.chain_state.block_hash(), - tx_index, - tx, - prior_cached_block.sapling_end_size, - prior_cached_block.orchard_end_size, - &mut self.rng, - ); - assert_eq!(cb.height(), height); - - let res = self.cache_block(&prior_cached_block, cb); - self.latest_block_height = Some(height); - - (height, res) - } -} - -impl TestState -where - Cache: TestCache, - ::Error: fmt::Debug, - ParamsT: consensus::Parameters + Send + 'static, - DbT: InputSource + WalletWrite + WalletCommitmentTrees, - ::AccountId: ConditionallySelectable + Default + Send + 'static, -{ - /// Invokes [`scan_cached_blocks`] with the given arguments, expecting success. - pub(crate) fn scan_cached_blocks( - &mut self, - from_height: BlockHeight, - limit: usize, - ) -> ScanSummary { - let result = self.try_scan_cached_blocks(from_height, limit); - assert_matches!(result, Ok(_)); - result.unwrap() - } - - /// Invokes [`scan_cached_blocks`] with the given arguments. - pub(crate) fn try_scan_cached_blocks( - &mut self, - from_height: BlockHeight, - limit: usize, - ) -> Result< - ScanSummary, - data_api::chain::error::Error< - ::Error, - ::Error, - >, - > { - let prior_cached_block = self - .latest_cached_block_below_height(from_height) - .cloned() - .unwrap_or_else(|| CachedBlock::none(from_height - 1)); - - let result = scan_cached_blocks( - &self.network, - self.cache.block_source(), - &mut self.wallet_data, - from_height, - &prior_cached_block.chain_state, - limit, - ); - result - } - - /// Insert shard roots for both trees. - pub(crate) fn put_subtree_roots( - &mut self, - sapling_start_index: u64, - sapling_roots: &[CommitmentTreeRoot], - #[cfg(feature = "orchard")] orchard_start_index: u64, - #[cfg(feature = "orchard")] orchard_roots: &[CommitmentTreeRoot], - ) -> Result<(), ShardTreeError<::Error>> { - self.wallet_mut() - .put_sapling_subtree_roots(sapling_start_index, sapling_roots)?; - - #[cfg(feature = "orchard")] - self.wallet_mut() - .put_orchard_subtree_roots(orchard_start_index, orchard_roots)?; - - Ok(()) - } -} - -impl TestState -where - ParamsT: consensus::Parameters + Send + 'static, - AccountIdT: std::cmp::Eq + std::hash::Hash, - ErrT: std::fmt::Debug, - DbT: InputSource - + WalletWrite - + WalletCommitmentTrees, - ::AccountId: ConditionallySelectable + Default + Send + 'static, -{ - /// Invokes [`create_spend_to_address`] with the given arguments. - #[allow(deprecated)] - #[allow(clippy::type_complexity)] - #[allow(clippy::too_many_arguments)] - pub(crate) fn create_spend_to_address( - &mut self, - usk: &UnifiedSpendingKey, - to: &Address, - amount: NonNegativeAmount, - memo: Option, - ovk_policy: OvkPolicy, - min_confirmations: NonZeroU32, - change_memo: Option, - fallback_change_pool: ShieldedProtocol, - ) -> Result< - NonEmpty, - data_api::error::Error< - ErrT, - ::Error, - GreedyInputSelectorError::NoteRef>, - Zip317FeeError, - >, - > { - let prover = test_prover(); - let network = self.network().clone(); - create_spend_to_address( - self.wallet_mut(), - &network, - &prover, - &prover, - usk, - to, - amount, - memo, - ovk_policy, - min_confirmations, - change_memo, - fallback_change_pool, - ) - } - - /// Invokes [`spend`] with the given arguments. - #[allow(clippy::type_complexity)] - pub(crate) fn spend( - &mut self, - input_selector: &InputsT, - usk: &UnifiedSpendingKey, - request: zip321::TransactionRequest, - ovk_policy: OvkPolicy, - min_confirmations: NonZeroU32, - ) -> Result< - NonEmpty, - data_api::error::Error< - ErrT, - ::Error, - InputsT::Error, - ::Error, - >, - > - where - InputsT: InputSelector, - { - #![allow(deprecated)] - let prover = test_prover(); - let network = self.network().clone(); - spend( - self.wallet_mut(), - &network, - &prover, - &prover, - input_selector, - usk, - request, - ovk_policy, - min_confirmations, - ) - } - - /// Invokes [`propose_transfer`] with the given arguments. - #[allow(clippy::type_complexity)] - pub(crate) fn propose_transfer( - &mut self, - spend_from_account: ::AccountId, - input_selector: &InputsT, - request: zip321::TransactionRequest, - min_confirmations: NonZeroU32, - ) -> Result< - Proposal::NoteRef>, - data_api::error::Error< - ErrT, - Infallible, - InputsT::Error, - ::Error, - >, - > - where - InputsT: InputSelector, - { - let network = self.network().clone(); - propose_transfer::<_, _, _, Infallible>( - self.wallet_mut(), - &network, - spend_from_account, - input_selector, - request, - min_confirmations, - ) - } - - /// Invokes [`propose_standard_transfer`] with the given arguments. - #[allow(clippy::type_complexity)] - #[allow(clippy::too_many_arguments)] - pub(crate) fn propose_standard_transfer( - &mut self, - spend_from_account: ::AccountId, - fee_rule: StandardFeeRule, - min_confirmations: NonZeroU32, - to: &Address, - amount: NonNegativeAmount, - memo: Option, - change_memo: Option, - fallback_change_pool: ShieldedProtocol, - ) -> Result< - Proposal::NoteRef>, - data_api::error::Error< - ErrT, - CommitmentTreeErrT, - GreedyInputSelectorError::NoteRef>, - Zip317FeeError, - >, - > { - let network = self.network().clone(); - let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>( - self.wallet_mut(), - &network, - fee_rule, - spend_from_account, - min_confirmations, - to, - amount, - memo, - change_memo, - fallback_change_pool, - ); - - if let Ok(proposal) = &result { - check_proposal_serialization_roundtrip(self.wallet(), proposal); - } - - result - } - - /// Invokes [`propose_shielding`] with the given arguments. - #[cfg(feature = "transparent-inputs")] - #[allow(clippy::type_complexity)] - #[allow(dead_code)] - pub(crate) fn propose_shielding( - &mut self, - input_selector: &InputsT, - shielding_threshold: NonNegativeAmount, - from_addrs: &[TransparentAddress], - min_confirmations: u32, - ) -> Result< - Proposal, - data_api::error::Error< - ErrT, - Infallible, - InputsT::Error, - ::Error, - >, - > - where - InputsT: ShieldingSelector, - { - let network = self.network().clone(); - propose_shielding::<_, _, _, Infallible>( - self.wallet_mut(), - &network, - input_selector, - shielding_threshold, - from_addrs, - min_confirmations, - ) - } - - /// Invokes [`create_proposed_transactions`] with the given arguments. - #[allow(clippy::type_complexity)] - pub(crate) fn create_proposed_transactions( - &mut self, - usk: &UnifiedSpendingKey, - ovk_policy: OvkPolicy, - proposal: &Proposal, - ) -> Result< - NonEmpty, - data_api::error::Error< - ErrT, - ::Error, - InputsErrT, - FeeRuleT::Error, - >, - > - where - FeeRuleT: FeeRule, - { - let prover = test_prover(); - let network = self.network().clone(); - create_proposed_transactions( - self.wallet_mut(), - &network, - &prover, - &prover, - usk, - ovk_policy, - proposal, - ) - } - - /// Invokes [`shield_transparent_funds`] with the given arguments. - #[cfg(feature = "transparent-inputs")] - #[allow(clippy::type_complexity)] - pub(crate) fn shield_transparent_funds( - &mut self, - input_selector: &InputsT, - shielding_threshold: NonNegativeAmount, - usk: &UnifiedSpendingKey, - from_addrs: &[TransparentAddress], - min_confirmations: u32, - ) -> Result< - NonEmpty, - data_api::error::Error< - ErrT, - ::Error, - InputsT::Error, - ::Error, - >, - > - where - InputsT: ShieldingSelector, - { - let prover = test_prover(); - let network = self.network().clone(); - shield_transparent_funds( - self.wallet_mut(), - &network, - &prover, - &prover, - input_selector, - shielding_threshold, - usk, - from_addrs, - min_confirmations, - ) - } - - fn with_account_balance T>( - &self, - account: AccountIdT, - min_confirmations: u32, - f: F, - ) -> T { - let binding = self - .wallet() - .get_wallet_summary(min_confirmations) - .unwrap() - .unwrap(); - f(binding.account_balances().get(&account).unwrap()) - } - - pub(crate) fn get_total_balance(&self, account: AccountIdT) -> NonNegativeAmount { - self.with_account_balance(account, 0, |balance| balance.total()) - } - - pub(crate) fn get_spendable_balance( - &self, - account: AccountIdT, - min_confirmations: u32, - ) -> NonNegativeAmount { - self.with_account_balance(account, min_confirmations, |balance| { - balance.spendable_value() - }) - } - - pub(crate) fn get_pending_shielded_balance( - &self, - account: AccountIdT, - min_confirmations: u32, - ) -> NonNegativeAmount { - self.with_account_balance(account, min_confirmations, |balance| { - balance.value_pending_spendability() + balance.change_pending_confirmation() - }) - .unwrap() - } - - #[allow(dead_code)] - pub(crate) fn get_pending_change( - &self, - account: AccountIdT, - min_confirmations: u32, - ) -> NonNegativeAmount { - self.with_account_balance(account, min_confirmations, |balance| { - balance.change_pending_confirmation() - }) - } - - pub(crate) fn get_wallet_summary( - &self, - min_confirmations: u32, - ) -> Option> { - self.wallet().get_wallet_summary(min_confirmations).unwrap() - } - - /// Returns a transaction from the history. - #[allow(dead_code)] - pub(crate) fn get_tx_from_history( - &self, - txid: TxId, - ) -> Result>, ErrT> { - let history = self.wallet().get_tx_history()?; - Ok(history.into_iter().find(|tx| tx.txid() == txid)) - } -} - -impl TestState { - /// Resets the wallet using a new wallet database but with the same cache of blocks, - /// and returns the old wallet database file. - /// - /// This does not recreate accounts, nor does it rescan the cached blocks. - /// The resulting wallet has no test account. - /// Before using any `generate_*` method on the reset state, call `reset_latest_cached_block()`. - pub(crate) fn reset(&mut self) -> DbT::Handle { - self.latest_block_height = None; - self.test_account = None; - DbT::reset(self) - } - - // /// Reset the latest cached block to the most recent one in the cache database. - // #[allow(dead_code)] - // pub(crate) fn reset_latest_cached_block(&mut self) { - // self.cache - // .block_source() - // .with_blocks::<_, Infallible>(None, None, |block: CompactBlock| { - // let chain_metadata = block.chain_metadata.unwrap(); - // self.latest_cached_block = Some(CachedBlock::at( - // BlockHash::from_slice(block.hash.as_slice()), - // BlockHeight::from_u32(block.height.try_into().unwrap()), - // chain_metadata.sapling_commitment_tree_size, - // chain_metadata.orchard_commitment_tree_size, - // )); - // Ok(()) - // }) - // .unwrap(); - // } -} - -/// Trait used by tests that require a full viewing key. -pub(crate) trait TestFvk { - type Nullifier: Copy; - - fn sapling_ovk(&self) -> Option; - - #[cfg(feature = "orchard")] - fn orchard_ovk(&self, scope: zip32::Scope) -> Option; - - fn add_spend( - &self, - ctx: &mut CompactTx, - nf: Self::Nullifier, - rng: &mut R, - ); - - #[allow(clippy::too_many_arguments)] - fn add_output( - &self, - ctx: &mut CompactTx, - params: &P, - height: BlockHeight, - req: AddressType, - value: NonNegativeAmount, - initial_sapling_tree_size: u32, - // we don't require an initial Orchard tree size because we don't need it to compute - // the nullifier. - rng: &mut R, - ) -> Self::Nullifier; - - #[allow(clippy::too_many_arguments)] - fn add_logical_action( - &self, - ctx: &mut CompactTx, - params: &P, - height: BlockHeight, - nf: Self::Nullifier, - req: AddressType, - value: NonNegativeAmount, - initial_sapling_tree_size: u32, - // we don't require an initial Orchard tree size because we don't need it to compute - // the nullifier. - rng: &mut R, - ) -> Self::Nullifier; -} - -impl<'a, A: TestFvk> TestFvk for &'a A { - type Nullifier = A::Nullifier; - - fn sapling_ovk(&self) -> Option { - (*self).sapling_ovk() - } - - #[cfg(feature = "orchard")] - fn orchard_ovk(&self, scope: zip32::Scope) -> Option { - (*self).orchard_ovk(scope) - } - - fn add_spend( - &self, - ctx: &mut CompactTx, - nf: Self::Nullifier, - rng: &mut R, - ) { - (*self).add_spend(ctx, nf, rng) - } - - fn add_output( - &self, - ctx: &mut CompactTx, - params: &P, - height: BlockHeight, - req: AddressType, - value: Zatoshis, - initial_sapling_tree_size: u32, - // we don't require an initial Orchard tree size because we don't need it to compute - // the nullifier. - rng: &mut R, - ) -> Self::Nullifier { - (*self).add_output( - ctx, - params, - height, - req, - value, - initial_sapling_tree_size, - rng, - ) - } - - fn add_logical_action( - &self, - ctx: &mut CompactTx, - params: &P, - height: BlockHeight, - nf: Self::Nullifier, - req: AddressType, - value: Zatoshis, - initial_sapling_tree_size: u32, - // we don't require an initial Orchard tree size because we don't need it to compute - // the nullifier. - rng: &mut R, - ) -> Self::Nullifier { - (*self).add_logical_action( - ctx, - params, - height, - nf, - req, - value, - initial_sapling_tree_size, - rng, - ) - } -} - -impl TestFvk for DiversifiableFullViewingKey { - type Nullifier = Nullifier; - - fn sapling_ovk(&self) -> Option { - Some(self.fvk().ovk) - } - - #[cfg(feature = "orchard")] - fn orchard_ovk(&self, _: zip32::Scope) -> Option { - None - } - - fn add_spend( - &self, - ctx: &mut CompactTx, - nf: Self::Nullifier, - _: &mut R, - ) { - let cspend = CompactSaplingSpend { nf: nf.to_vec() }; - ctx.spends.push(cspend); - } - - fn add_output( - &self, - ctx: &mut CompactTx, - params: &P, - height: BlockHeight, - req: AddressType, - value: NonNegativeAmount, - initial_sapling_tree_size: u32, - rng: &mut R, - ) -> Self::Nullifier { - let recipient = match req { - AddressType::DefaultExternal => self.default_address().1, - AddressType::DiversifiedExternal(idx) => self.find_address(idx).unwrap().1, - AddressType::Internal => self.change_address().1, - }; - - let position = initial_sapling_tree_size + ctx.outputs.len() as u32; - - let (cout, note) = - compact_sapling_output(params, height, recipient, value, self.sapling_ovk(), rng); - ctx.outputs.push(cout); - - note.nf(&self.fvk().vk.nk, position as u64) - } - - #[allow(clippy::too_many_arguments)] - fn add_logical_action( - &self, - ctx: &mut CompactTx, - params: &P, - height: BlockHeight, - nf: Self::Nullifier, - req: AddressType, - value: NonNegativeAmount, - initial_sapling_tree_size: u32, - rng: &mut R, - ) -> Self::Nullifier { - self.add_spend(ctx, nf, rng); - self.add_output( - ctx, - params, - height, - req, - value, - initial_sapling_tree_size, - rng, - ) - } -} - -#[cfg(feature = "orchard")] -impl TestFvk for orchard::keys::FullViewingKey { - type Nullifier = orchard::note::Nullifier; - - fn sapling_ovk(&self) -> Option { - None - } - - fn orchard_ovk(&self, scope: zip32::Scope) -> Option { - Some(self.to_ovk(scope)) - } - - fn add_spend( - &self, - ctx: &mut CompactTx, - revealed_spent_note_nullifier: Self::Nullifier, - rng: &mut R, - ) { - // Generate a dummy recipient. - let recipient = loop { - let mut bytes = [0; 32]; - rng.fill_bytes(&mut bytes); - let sk = orchard::keys::SpendingKey::from_bytes(bytes); - if sk.is_some().into() { - break orchard::keys::FullViewingKey::from(&sk.unwrap()) - .address_at(0u32, zip32::Scope::External); - } - }; - - let (cact, _) = compact_orchard_action( - revealed_spent_note_nullifier, - recipient, - NonNegativeAmount::ZERO, - self.orchard_ovk(zip32::Scope::Internal), - rng, - ); - ctx.actions.push(cact); - } - - fn add_output( - &self, - ctx: &mut CompactTx, - _: &P, - _: BlockHeight, - req: AddressType, - value: NonNegativeAmount, - _: u32, // the position is not required for computing the Orchard nullifier - mut rng: &mut R, - ) -> Self::Nullifier { - // Generate a dummy nullifier for the spend - let revealed_spent_note_nullifier = - orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr()) - .unwrap(); - - let (j, scope) = match req { - AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External), - AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External), - AddressType::Internal => (0u32.into(), zip32::Scope::Internal), - }; - - let (cact, note) = compact_orchard_action( - revealed_spent_note_nullifier, - self.address_at(j, scope), - value, - self.orchard_ovk(scope), - rng, - ); - ctx.actions.push(cact); - - note.nullifier(self) - } - - // Override so we can merge the spend and output into a single action. - fn add_logical_action( - &self, - ctx: &mut CompactTx, - _: &P, - _: BlockHeight, - revealed_spent_note_nullifier: Self::Nullifier, - address_type: AddressType, - value: NonNegativeAmount, - _: u32, // the position is not required for computing the Orchard nullifier - rng: &mut R, - ) -> Self::Nullifier { - let (j, scope) = match address_type { - AddressType::DefaultExternal => (0u32.into(), zip32::Scope::External), - AddressType::DiversifiedExternal(idx) => (idx, zip32::Scope::External), - AddressType::Internal => (0u32.into(), zip32::Scope::Internal), - }; - - let (cact, note) = compact_orchard_action( - revealed_spent_note_nullifier, - self.address_at(j, scope), - value, - self.orchard_ovk(scope), - rng, - ); - ctx.actions.push(cact); - - // Return the nullifier of the newly created output note - note.nullifier(self) - } -} - -#[derive(Clone, Copy)] -pub(crate) enum AddressType { - DefaultExternal, - #[allow(dead_code)] - DiversifiedExternal(DiversifierIndex), - Internal, -} - -/// Creates a `CompactSaplingOutput` at the given height paying the given recipient. -/// -/// Returns the `CompactSaplingOutput` and the new note. -fn compact_sapling_output( - params: &P, - height: BlockHeight, - recipient: sapling::PaymentAddress, - value: NonNegativeAmount, - ovk: Option, - rng: &mut R, -) -> (CompactSaplingOutput, sapling::Note) { - let rseed = generate_random_rseed(zip212_enforcement(params, height), rng); - let note = Note::from_parts( - recipient, - sapling::value::NoteValue::from_raw(value.into_u64()), - rseed, - ); - let encryptor = sapling_note_encryption(ovk, note.clone(), *MemoBytes::empty().as_array(), rng); - let cmu = note.cmu().to_bytes().to_vec(); - let ephemeral_key = SaplingDomain::epk_bytes(encryptor.epk()).0.to_vec(); - let enc_ciphertext = encryptor.encrypt_note_plaintext(); - - ( - CompactSaplingOutput { - cmu, - ephemeral_key, - ciphertext: enc_ciphertext.as_ref()[..52].to_vec(), - }, - note, - ) -} - -/// Creates a `CompactOrchardAction` at the given height paying the given recipient. -/// -/// Returns the `CompactOrchardAction` and the new note. -#[cfg(feature = "orchard")] -fn compact_orchard_action( - nf_old: orchard::note::Nullifier, - recipient: orchard::Address, - value: NonNegativeAmount, - ovk: Option, - rng: &mut R, -) -> (CompactOrchardAction, orchard::Note) { - use zcash_note_encryption::ShieldedOutput; - - let (compact_action, note) = orchard::note_encryption::testing::fake_compact_action( - rng, - nf_old, - recipient, - orchard::value::NoteValue::from_raw(value.into_u64()), - ovk, - ); - - ( - CompactOrchardAction { - nullifier: compact_action.nullifier().to_bytes().to_vec(), - cmx: compact_action.cmx().to_bytes().to_vec(), - ephemeral_key: compact_action.ephemeral_key().0.to_vec(), - ciphertext: compact_action.enc_ciphertext().as_ref()[..52].to_vec(), - }, - note, - ) -} - -/// Creates a fake `CompactTx` with a random transaction ID and no spends or outputs. -fn fake_compact_tx(rng: &mut R) -> CompactTx { - let mut ctx = CompactTx::default(); - let mut txid = vec![0; 32]; - rng.fill_bytes(&mut txid); - ctx.hash = txid; - - ctx -} - -#[derive(Clone)] -pub(crate) struct FakeCompactOutput { - fvk: Fvk, - address_type: AddressType, - value: NonNegativeAmount, -} - -impl FakeCompactOutput { - pub(crate) fn new(fvk: Fvk, address_type: AddressType, value: NonNegativeAmount) -> Self { - Self { - fvk, - address_type, - value, - } - } -} - -/// Create a fake CompactBlock at the given height, containing the specified fake compact outputs. -/// -/// Returns the newly created compact block, along with the nullifier for each note created in that -/// block. -#[allow(clippy::too_many_arguments)] -fn fake_compact_block( - params: &P, - height: BlockHeight, - prev_hash: BlockHash, - outputs: &[FakeCompactOutput], - initial_sapling_tree_size: u32, - initial_orchard_tree_size: u32, - mut rng: impl RngCore + CryptoRng, -) -> (CompactBlock, Vec) { - // Create a fake CompactBlock containing the note - let mut ctx = fake_compact_tx(&mut rng); - let mut nfs = vec![]; - for output in outputs { - let nf = output.fvk.add_output( - &mut ctx, - params, - height, - output.address_type, - output.value, - initial_sapling_tree_size, - &mut rng, - ); - nfs.push(nf); - } - - let cb = fake_compact_block_from_compact_tx( - ctx, - height, - prev_hash, - initial_sapling_tree_size, - initial_orchard_tree_size, - rng, - ); - (cb, nfs) -} - -/// Create a fake CompactBlock at the given height containing only the given transaction. -fn fake_compact_block_from_tx( - height: BlockHeight, - prev_hash: BlockHash, - tx_index: usize, - tx: &Transaction, - initial_sapling_tree_size: u32, - initial_orchard_tree_size: u32, - rng: impl RngCore, -) -> CompactBlock { - // Create a fake CompactTx containing the transaction. - let mut ctx = CompactTx { - index: tx_index as u64, - hash: tx.txid().as_ref().to_vec(), - ..Default::default() - }; - - if let Some(bundle) = tx.sapling_bundle() { - for spend in bundle.shielded_spends() { - ctx.spends.push(spend.into()); - } - for output in bundle.shielded_outputs() { - ctx.outputs.push(output.into()); - } - } - - #[cfg(feature = "orchard")] - if let Some(bundle) = tx.orchard_bundle() { - for action in bundle.actions() { - ctx.actions.push(action.into()); - } - } - - fake_compact_block_from_compact_tx( - ctx, - height, - prev_hash, - initial_sapling_tree_size, - initial_orchard_tree_size, - rng, - ) -} - -/// Create a fake CompactBlock at the given height, spending a single note from the -/// given address. -#[allow(clippy::too_many_arguments)] -fn fake_compact_block_spending( - params: &P, - height: BlockHeight, - prev_hash: BlockHash, - (nf, in_value): (Fvk::Nullifier, NonNegativeAmount), - fvk: &Fvk, - to: Address, - value: NonNegativeAmount, - initial_sapling_tree_size: u32, - initial_orchard_tree_size: u32, - mut rng: impl RngCore + CryptoRng, -) -> CompactBlock { - let mut ctx = fake_compact_tx(&mut rng); - - // Create a fake spend and a fake Note for the change - fvk.add_logical_action( - &mut ctx, - params, - height, - nf, - AddressType::Internal, - (in_value - value).unwrap(), - initial_sapling_tree_size, - &mut rng, - ); - - // Create a fake Note for the payment - match to { - Address::Sapling(recipient) => ctx.outputs.push( - compact_sapling_output( - params, - height, - recipient, - value, - fvk.sapling_ovk(), - &mut rng, - ) - .0, - ), - Address::Transparent(_) | Address::Tex(_) => { - panic!("transparent addresses not supported in compact blocks") - } - Address::Unified(ua) => { - // This is annoying to implement, because the protocol-aware UA type has no - // concept of ZIP 316 preference order. - let mut done = false; - - #[cfg(feature = "orchard")] - if let Some(recipient) = ua.orchard() { - // Generate a dummy nullifier - let nullifier = - orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr()) - .unwrap(); - - ctx.actions.push( - compact_orchard_action( - nullifier, - *recipient, - value, - fvk.orchard_ovk(zip32::Scope::External), - &mut rng, - ) - .0, - ); - done = true; - } - - if !done { - if let Some(recipient) = ua.sapling() { - ctx.outputs.push( - compact_sapling_output( - params, - height, - *recipient, - value, - fvk.sapling_ovk(), - &mut rng, - ) - .0, - ); - done = true; - } - } - if !done { - panic!("No supported shielded receiver to send funds to"); - } - } - } - - fake_compact_block_from_compact_tx( - ctx, - height, - prev_hash, - initial_sapling_tree_size, - initial_orchard_tree_size, - rng, - ) -} - -fn fake_compact_block_from_compact_tx( - ctx: CompactTx, - height: BlockHeight, - prev_hash: BlockHash, - initial_sapling_tree_size: u32, - initial_orchard_tree_size: u32, - mut rng: impl RngCore, -) -> CompactBlock { - let mut cb = CompactBlock { - hash: { - let mut hash = vec![0; 32]; - rng.fill_bytes(&mut hash); - hash - }, - height: height.into(), - ..Default::default() - }; - cb.prev_hash.extend_from_slice(&prev_hash.0); - cb.vtx.push(ctx); - cb.chain_metadata = Some(compact::ChainMetadata { - sapling_commitment_tree_size: initial_sapling_tree_size - + cb.vtx.iter().map(|tx| tx.outputs.len() as u32).sum::(), - orchard_commitment_tree_size: initial_orchard_tree_size - + cb.vtx.iter().map(|tx| tx.actions.len() as u32).sum::(), - }); - cb -} - -/// Trait used by tests that require a block cache. -pub(crate) trait TestCache { - type BlockSource: BlockSource; - type InsertResult; - - /// Exposes the block cache as a [`BlockSource`]. - fn block_source(&self) -> &Self::BlockSource; - - /// Inserts a CompactBlock into the cache DB. - fn insert(&self, cb: &CompactBlock) -> Self::InsertResult; -} - pub(crate) struct BlockCache { _cache_file: NamedTempFile, db_cache: BlockDb, @@ -1896,48 +43,6 @@ impl BlockCache { } } -pub(crate) struct NoteCommitments { - sapling: Vec, - #[cfg(feature = "orchard")] - orchard: Vec, -} - -impl NoteCommitments { - pub(crate) fn from_compact_block(cb: &CompactBlock) -> Self { - NoteCommitments { - sapling: cb - .vtx - .iter() - .flat_map(|tx| { - tx.outputs - .iter() - .map(|out| sapling::Node::from_cmu(&out.cmu().unwrap())) - }) - .collect(), - #[cfg(feature = "orchard")] - orchard: cb - .vtx - .iter() - .flat_map(|tx| { - tx.actions - .iter() - .map(|act| MerkleHashOrchard::from_cmx(&act.cmx().unwrap())) - }) - .collect(), - } - } - - #[allow(dead_code)] - pub(crate) fn sapling(&self) -> &[sapling::Node] { - self.sapling.as_ref() - } - - #[cfg(feature = "orchard")] - pub(crate) fn orchard(&self) -> &[MerkleHashOrchard] { - self.orchard.as_ref() - } -} - impl TestCache for BlockCache { type BlockSource = BlockDb; type InsertResult = NoteCommitments; @@ -2010,25 +115,3 @@ impl TestCache for FsBlockCache { meta } } - -pub(crate) fn input_selector( - fee_rule: StandardFeeRule, - change_memo: Option<&str>, - fallback_change_pool: ShieldedProtocol, -) -> GreedyInputSelector { - let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::().unwrap())); - let change_strategy = - standard::SingleOutputChangeStrategy::new(fee_rule, change_memo, fallback_change_pool); - GreedyInputSelector::new(change_strategy, DustOutputPolicy::default()) -} - -// Checks that a protobuf proposal serialized from the provided proposal value correctly parses to -// the same proposal value. -fn check_proposal_serialization_roundtrip( - wallet_data: &DbT, - proposal: &Proposal, -) { - let proposal_proto = proposal::Proposal::from_standard_proposal(proposal); - let deserialized_proposal = proposal_proto.try_into_standard_proposal(wallet_data); - assert_matches!(deserialized_proposal, Ok(r) if &r == proposal); -} diff --git a/zcash_client_sqlite/src/testing/db.rs b/zcash_client_sqlite/src/testing/db.rs index cb71211845..745255d206 100644 --- a/zcash_client_sqlite/src/testing/db.rs +++ b/zcash_client_sqlite/src/testing/db.rs @@ -14,6 +14,7 @@ use zcash_client_backend::{ data_api::{ chain::{ChainState, CommitmentTreeRoot}, scanning::ScanRange, + testing::{DataStoreFactory, Reset, TestState}, *, }, keys::UnifiedFullViewingKey, @@ -30,13 +31,12 @@ use zcash_primitives::{ }; use zcash_protocol::{consensus::BlockHeight, local_consensus::LocalNetwork, memo::Memo}; -use super::{DataStoreFactory, Reset, TestState}; use crate::{wallet::init::init_wallet_db, AccountId, WalletDb}; #[cfg(feature = "transparent-inputs")] use { - core::ops::Range, crate::TransparentAddressMetadata, + core::ops::Range, zcash_primitives::{legacy::TransparentAddress, transaction::components::OutPoint}, }; @@ -165,7 +165,7 @@ impl Reset for TestDb { fn reset(st: &mut TestState) -> NamedTempFile { let network = *st.network(); let old_db = std::mem::replace( - &mut st.wallet_data, + st.wallet_mut(), TestDbFactory.new_data_store(network).unwrap(), ); old_db.take_data_file() diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index daaaf41d12..6e76485cc9 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -33,6 +33,10 @@ use zcash_client_backend::{ self, chain::{self, ChainState, CommitmentTreeRoot, ScanSummary}, error::Error, + testing::{ + input_selector, AddressType, FakeCompactOutput, InitialChainState, TestBuilder, + TestFvk, TestState, + }, wallet::{ decrypt_and_store_transaction, input_selection::{GreedyInputSelector, GreedyInputSelectorError}, @@ -50,13 +54,11 @@ use zcash_client_backend::{ }; use zcash_protocol::consensus::{self, BlockHeight}; -use super::TestFvk; use crate::{ error::SqliteClientError, testing::{ db::{TestDb, TestDbFactory}, - input_selector, AddressType, BlockCache, FakeCompactOutput, InitialChainState, TestBuilder, - TestState, + BlockCache, }, wallet::{commitment_tree, parse_scope, truncate_to_height}, AccountId, NoteId, ReceivedNoteId, @@ -319,11 +321,10 @@ pub(crate) fn send_multi_step_proposed_transfer() { legacy::keys::{NonHardenedChildIndex, TransparentKeyScope}, transaction::builder::{BuildConfig, Builder}, }; + use zcash_proofs::prover::LocalTxProver; use zcash_protocol::value::ZatBalance; - use crate::wallet::{ - sapling::tests::test_prover, transparent::get_wallet_transparent_output, GAP_LIMIT, - }; + use crate::wallet::{transparent::get_wallet_transparent_output, GAP_LIMIT}; let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) @@ -593,7 +594,7 @@ pub(crate) fn send_multi_step_proposed_transfer() { .unwrap(); assert_matches!(builder.add_transparent_input(sk, outpoint, txout), Ok(_)); - let test_prover = test_prover(); + let test_prover = LocalTxProver::bundled(); let build_result = builder .build( OsRng, @@ -1750,8 +1751,8 @@ pub(crate) fn checkpoint_gaps() { AddressType::DefaultExternal, not_our_value, )], - st.latest_cached_block().unwrap().sapling_end_size, - st.latest_cached_block().unwrap().orchard_end_size, + st.latest_cached_block().unwrap().sapling_end_size(), + st.latest_cached_block().unwrap().orchard_end_size(), false, ); @@ -2106,7 +2107,7 @@ pub(crate) fn multi_pool_checkpoint impl SpendProver + OutputProver { - LocalTxProver::bundled() - } - #[test] fn send_single_step_proposed_transfer() { testing::pool::send_single_step_proposed_transfer::() diff --git a/zcash_client_sqlite/src/wallet/scanning.rs b/zcash_client_sqlite/src/wallet/scanning.rs index 10ff63abbe..6e48069389 100644 --- a/zcash_client_sqlite/src/wallet/scanning.rs +++ b/zcash_client_sqlite/src/wallet/scanning.rs @@ -587,6 +587,7 @@ pub(crate) mod tests { use zcash_client_backend::data_api::{ chain::{ChainState, CommitmentTreeRoot}, scanning::{spanning_tree::testing::scan_range, ScanPriority}, + testing::{AddressType, FakeCompactOutput, InitialChainState, TestBuilder, TestState}, AccountBirthday, Ratio, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT, }; use zcash_primitives::{ @@ -601,7 +602,7 @@ pub(crate) mod tests { testing::{ db::{TestDb, TestDbFactory}, pool::ShieldedPoolTester, - AddressType, BlockCache, FakeCompactOutput, InitialChainState, TestBuilder, TestState, + BlockCache, }, wallet::{ sapling::tests::SaplingPoolTester, diff --git a/zcash_client_sqlite/src/wallet/transparent.rs b/zcash_client_sqlite/src/wallet/transparent.rs index fa14de41f0..62750837d0 100644 --- a/zcash_client_sqlite/src/wallet/transparent.rs +++ b/zcash_client_sqlite/src/wallet/transparent.rs @@ -824,14 +824,15 @@ pub(crate) fn queue_transparent_spend_detection( mod tests { use crate::testing::{ db::{TestDb, TestDbFactory}, - AddressType, BlockCache, TestBuilder, TestState, + BlockCache, }; use sapling::zip32::ExtendedSpendingKey; use zcash_client_backend::{ data_api::{ - wallet::input_selection::GreedyInputSelector, Account as _, InputSource, WalletRead, - WalletWrite, + testing::{AddressType, TestBuilder, TestState}, + wallet::input_selection::GreedyInputSelector, + Account as _, InputSource, WalletRead, WalletWrite, }, encoding::AddressCodec, fees::{fixed, DustOutputPolicy}, @@ -847,8 +848,6 @@ 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])) From 2be0dfbc0cd1427d3879a0bcb77730fca58408cd Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 5 Sep 2024 22:05:48 -0600 Subject: [PATCH 12/20] zcash_client_backend: Record audit of `ambassador` crate. --- supply-chain/audits.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index 83fa292747..6b2c550c6a 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -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 " +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 " criteria = "safe-to-deploy" From f2654f5bf1cff7392a83b4bdd8efeacd6c2e3d77 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 6 Sep 2024 09:08:41 -0600 Subject: [PATCH 13/20] zcash_client_backend: Fix broken intra-doc links and other doc warnings. --- components/zip321/src/lib.rs | 2 +- zcash_client_backend/src/data_api/testing.rs | 23 ++++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/components/zip321/src/lib.rs b/components/zip321/src/lib.rs index 91974da43a..04e1dfb940 100644 --- a/components/zip321/src/lib.rs +++ b/components/zip321/src/lib.rs @@ -654,7 +654,7 @@ mod parse { )(input) } - /// The primary parser for = 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)), diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs index a0fabf74a4..8534a389c7 100644 --- a/zcash_client_backend/src/data_api/testing.rs +++ b/zcash_client_backend/src/data_api/testing.rs @@ -1,4 +1,4 @@ -//! Utilities for testing wallets based upon the [`zcash_client_backend::super`] traits. +//! Utilities for testing wallets based upon the [`crate::data_api`] traits. use assert_matches::assert_matches; use core::fmt; use group::ff::Field; @@ -368,7 +368,7 @@ impl .expect("NU5 activation height must be known.") } - /// Exposes the test seed, if enabled via [`TestBuilder::with_test_account`]. + /// Exposes the seed for the test wallet. pub fn test_seed(&self) -> Option<&SecretVec> { self.test_account.as_ref().map(|(seed, _)| seed) } @@ -379,19 +379,19 @@ where Network: consensus::Parameters, DataStore: WalletRead, { - /// Exposes the test account, if enabled via [`TestBuilder::with_test_account`]. + /// Returns a reference to the test account, if one was configured. pub fn test_account(&self) -> Option<&TestAccount<::Account>> { self.test_account.as_ref().map(|(_, acct)| acct) } - /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. + /// Returns the test account's Sapling DFVK, if one was configured. pub fn test_account_sapling(&self) -> Option<&DiversifiableFullViewingKey> { let (_, acct) = self.test_account.as_ref()?; let ufvk = acct.ufvk()?; ufvk.sapling() } - /// Exposes the test account's Sapling DFVK, if enabled via [`TestBuilder::with_test_account`]. + /// Returns the test account's Orchard FVK, if one was configured. #[cfg(feature = "orchard")] pub fn test_account_orchard(&self) -> Option<&orchard::keys::FullViewingKey> { let (_, acct) = self.test_account.as_ref()?; @@ -866,7 +866,7 @@ where ) } - /// Invokes [`propose_standard_transfer`] with the given arguments. + /// Invokes [`propose_standard_transfer_to_address`] with the given arguments. #[allow(clippy::type_complexity)] #[allow(clippy::too_many_arguments)] pub fn propose_standard_transfer( @@ -910,6 +910,8 @@ where } /// Invokes [`propose_shielding`] with the given arguments. + /// + /// [`propose_shielding`]: crate::data_api::wallet::propose_shielding #[cfg(feature = "transparent-inputs")] #[allow(clippy::type_complexity)] #[allow(dead_code)] @@ -972,6 +974,8 @@ where } /// Invokes [`shield_transparent_funds`] with the given arguments. + /// + /// [`shield_transparent_funds`]: crate::data_api::wallet::shield_transparent_funds #[cfg(feature = "transparent-inputs")] #[allow(clippy::type_complexity)] pub fn shield_transparent_funds( @@ -1194,7 +1198,7 @@ impl Default for TestBuilder<(), ()> { } impl TestBuilder<(), A> { - /// Adds a [`BlockDb`] cache to the test. + /// Adds a block cache to the test environment. pub fn with_block_cache(self, cache: C) -> TestBuilder { TestBuilder { rng: self.rng, @@ -1265,9 +1269,10 @@ impl TestBuilder { self } - /// Sets the [`account_index`] field for the test account + /// Sets the account index for the test account. /// - /// Call either [`with_account_from_sapling_activation`] or [`with_account_having_current_birthday`] before calling this method. + /// Call either [`Self::with_account_from_sapling_activation`] or + /// [`Self::with_account_having_current_birthday`] before calling this method. pub fn set_account_index(mut self, index: zip32::AccountId) -> Self { assert!(self.account_index.is_none()); self.account_index = Some(index); From 49dffbf6ee29970c364ecf77a96d9ecb80c59965 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 6 Sep 2024 14:00:39 -0600 Subject: [PATCH 14/20] zcash_client_sqlite: Remove unused `OutputRecoveryError` type. --- zcash_client_backend/src/data_api/testing.rs | 2 ++ zcash_client_sqlite/src/testing/pool.rs | 11 ++--------- zcash_client_sqlite/src/wallet/orchard.rs | 13 +++++-------- zcash_client_sqlite/src/wallet/sapling.rs | 13 +++++-------- 4 files changed, 14 insertions(+), 25 deletions(-) diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs index 8534a389c7..9de8a3eff0 100644 --- a/zcash_client_backend/src/data_api/testing.rs +++ b/zcash_client_backend/src/data_api/testing.rs @@ -80,6 +80,8 @@ use { group::ff::PrimeField, orchard::tree::MerkleHashOrchard, pasta_curves::pallas, }; +pub mod pool; + pub struct TransactionSummary { account_id: AccountId, txid: TxId, diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index 6e76485cc9..61f4cc8e04 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -76,13 +76,6 @@ use { #[cfg(feature = "orchard")] use zcash_client_backend::PoolType; -pub(crate) type OutputRecoveryError = Error< - SqliteClientError, - commitment_tree::Error, - GreedyInputSelectorError, - Zip317FeeError, ->; - /// Trait that exposes the pool-specific types and operations necessary to run the /// single-shielded-pool tests on a given pool. pub(crate) trait ShieldedPoolTester { @@ -149,7 +142,7 @@ pub(crate) trait ShieldedPoolTester { height: BlockHeight, tx: &Transaction, fvk: &Self::Fvk, - ) -> Result, OutputRecoveryError>; + ) -> Option<(Note, Address, MemoBytes)>; fn received_note_count(summary: &ScanSummary) -> usize; } @@ -1224,7 +1217,7 @@ pub(crate) fn ovk_policy_prevents_recovery_from_chain() { .unwrap(); let tx = Transaction::read(&raw_tx[..], BranchId::Canopy).unwrap(); - T::try_output_recovery(st.network(), h1, &tx, &dfvk) + Ok(T::try_output_recovery(st.network(), h1, &tx, &dfvk)) }; // Send some of the funds to another address, keeping history. diff --git a/zcash_client_sqlite/src/wallet/orchard.rs b/zcash_client_sqlite/src/wallet/orchard.rs index b92add9545..924c6a4261 100644 --- a/zcash_client_sqlite/src/wallet/orchard.rs +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -416,10 +416,7 @@ pub(crate) mod tests { }; use crate::{ - testing::{ - self, - pool::{OutputRecoveryError, ShieldedPoolTester}, - }, + testing::{self, pool::ShieldedPoolTester}, wallet::sapling::tests::SaplingPoolTester, ORCHARD_TABLES_PREFIX, }; @@ -537,7 +534,7 @@ pub(crate) mod tests { _: BlockHeight, tx: &Transaction, fvk: &Self::Fvk, - ) -> Result, OutputRecoveryError> { + ) -> 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( @@ -549,7 +546,7 @@ pub(crate) mod tests { ); if result.is_some() { - return Ok(result.map(|(note, addr, memo)| { + return result.map(|(note, addr, memo)| { ( Note::Orchard(note), UnifiedAddress::from_receivers(Some(addr), None, None) @@ -557,11 +554,11 @@ pub(crate) mod tests { .into(), MemoBytes::from_bytes(&memo).expect("correct length"), ) - })); + }); } } - Ok(None) + None } fn received_note_count( diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs index f0bbecc6c0..07ffde10a0 100644 --- a/zcash_client_sqlite/src/wallet/sapling.rs +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -432,10 +432,7 @@ pub(crate) mod tests { use zcash_protocol::consensus; use crate::{ - testing::{ - self, - pool::{OutputRecoveryError, ShieldedPoolTester}, - }, + testing::{self, pool::ShieldedPoolTester}, AccountId, SAPLING_TABLES_PREFIX, }; @@ -538,7 +535,7 @@ pub(crate) mod tests { height: BlockHeight, tx: &Transaction, fvk: &Self::Fvk, - ) -> Result, OutputRecoveryError> { + ) -> 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( @@ -548,17 +545,17 @@ pub(crate) mod tests { ); if result.is_some() { - return Ok(result.map(|(note, addr, memo)| { + return result.map(|(note, addr, memo)| { ( Note::Sapling(note), addr.into(), MemoBytes::from_bytes(&memo).expect("correct length"), ) - })); + }); } } - Ok(None) + None } fn received_note_count( From 4f5b3efe09d299e518706e880b4fbb35d6de8536 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 6 Sep 2024 14:47:52 -0600 Subject: [PATCH 15/20] zcash_client_backend: Move the `ShieldedPoolTester` trait from `zcash_client_sqlite` --- .../src/data_api/testing/pool.rs | 90 ++++++++++++++++ zcash_client_backend/src/decrypt.rs | 2 +- zcash_client_sqlite/src/testing/pool.rs | 100 +++--------------- .../src/wallet/commitment_tree.rs | 10 +- zcash_client_sqlite/src/wallet/orchard.rs | 25 +++-- zcash_client_sqlite/src/wallet/sapling.rs | 45 ++++---- zcash_client_sqlite/src/wallet/scanning.rs | 6 +- 7 files changed, 156 insertions(+), 122 deletions(-) create mode 100644 zcash_client_backend/src/data_api/testing/pool.rs diff --git a/zcash_client_backend/src/data_api/testing/pool.rs b/zcash_client_backend/src/data_api/testing/pool.rs new file mode 100644 index 0000000000..3e99306c33 --- /dev/null +++ b/zcash_client_backend/src/data_api/testing/pool.rs @@ -0,0 +1,90 @@ +use std::{cmp::Eq, hash::Hash}; + +use incrementalmerkletree::Level; +use rand::RngCore; +use shardtree::error::ShardTreeError; +use zcash_keys::{address::Address, keys::UnifiedSpendingKey}; +use zcash_primitives::transaction::Transaction; +use zcash_protocol::{ + consensus::{self, BlockHeight}, + memo::MemoBytes, + value::Zatoshis, + ShieldedProtocol, +}; + +use crate::{ + data_api::{ + chain::{CommitmentTreeRoot, ScanSummary}, + DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, WalletSummary, + }, + wallet::{Note, ReceivedNote}, +}; + +use super::{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( + st: &TestState, + ) -> 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( + st: &mut TestState, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError<::Error>>; + + fn next_subtree_index(s: &WalletSummary) -> u64; + + #[allow(clippy::type_complexity)] + fn select_spendable_notes( + st: &TestState, + account: ::AccountId, + target_value: Zatoshis, + anchor_height: BlockHeight, + exclude: &[DbT::NoteRef], + ) -> Result>, ::Error>; + + fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, A>) -> usize; + + fn with_decrypted_pool_memos(d_tx: &DecryptedTransaction<'_, A>, f: impl FnMut(&MemoBytes)); + + fn try_output_recovery( + params: &P, + height: BlockHeight, + tx: &Transaction, + fvk: &Self::Fvk, + ) -> Option<(Note, Address, MemoBytes)>; + + fn received_note_count(summary: &ScanSummary) -> usize; +} diff --git a/zcash_client_backend/src/decrypt.rs b/zcash_client_backend/src/decrypt.rs index b48d077b53..21ceeb5973 100644 --- a/zcash_client_backend/src/decrypt.rs +++ b/zcash_client_backend/src/decrypt.rs @@ -38,7 +38,7 @@ pub struct DecryptedOutput { transfer_type: TransferType, } -impl DecryptedOutput { +impl DecryptedOutput { pub fn new( index: usize, note: Note, diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index 61f4cc8e04..a76f409ddf 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -7,11 +7,11 @@ use std::{ num::{NonZeroU32, NonZeroU8}, }; -use incrementalmerkletree::{frontier::Frontier, Level}; -use rand_core::RngCore; +use incrementalmerkletree::frontier::Frontier; + use rusqlite::params; use secrecy::Secret; -use shardtree::error::ShardTreeError; + use zcash_primitives::{ block::BlockHash, consensus::{BranchId, NetworkUpgrade, Parameters}, @@ -31,28 +31,25 @@ use zcash_client_backend::{ address::Address, data_api::{ self, - chain::{self, ChainState, CommitmentTreeRoot, ScanSummary}, + chain::{self, ChainState, CommitmentTreeRoot}, error::Error, testing::{ - input_selector, AddressType, FakeCompactOutput, InitialChainState, TestBuilder, - TestFvk, TestState, + input_selector, pool::ShieldedPoolTester, AddressType, FakeCompactOutput, + InitialChainState, TestBuilder, TestState, }, wallet::{ decrypt_and_store_transaction, input_selection::{GreedyInputSelector, GreedyInputSelectorError}, }, - Account as _, AccountBirthday, DecryptedTransaction, InputSource, Ratio, - WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, + Account as _, AccountBirthday, Ratio, WalletRead, WalletWrite, }, decrypt_transaction, fees::{fixed, standard, DustOutputPolicy}, keys::UnifiedSpendingKey, scanning::ScanError, - wallet::{Note, OvkPolicy, ReceivedNote}, + wallet::{Note, OvkPolicy}, zip321::{self, Payment, TransactionRequest}, - ShieldedProtocol, }; -use zcash_protocol::consensus::{self, BlockHeight}; use crate::{ error::SqliteClientError, @@ -61,12 +58,13 @@ use crate::{ BlockCache, }, wallet::{commitment_tree, parse_scope, truncate_to_height}, - AccountId, NoteId, ReceivedNoteId, + NoteId, ReceivedNoteId, }; #[cfg(feature = "transparent-inputs")] use { - zcash_client_backend::wallet::WalletTransparentOutput, + crate::AccountId, + zcash_client_backend::{data_api::DecryptedTransaction, wallet::WalletTransparentOutput}, zcash_primitives::transaction::{ components::{OutPoint, TxOut}, fees::zip317, @@ -74,77 +72,13 @@ use { }; #[cfg(feature = "orchard")] -use zcash_client_backend::PoolType; +use { + zcash_client_backend::PoolType, + zcash_protocol::{consensus::BlockHeight, ShieldedProtocol}, +}; -/// Trait that exposes the pool-specific types and operations necessary to run the -/// single-shielded-pool tests on a given pool. -pub(crate) trait ShieldedPoolTester { - const SHIELDED_PROTOCOL: ShieldedProtocol; +pub(crate) trait ShieldedPoolPersistence { const TABLES_PREFIX: &'static str; - - type Sk; - type Fvk: TestFvk; - type MerkleTreeHash; - type Note; - - fn test_account_fvk( - st: &TestState, - ) -> 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( - st: &mut TestState, - start_index: u64, - roots: &[CommitmentTreeRoot], - ) -> Result<(), ShardTreeError<::Error>>; - - fn next_subtree_index(s: &WalletSummary) -> u64; - - #[allow(clippy::type_complexity)] - fn select_spendable_notes( - st: &TestState, - account: ::AccountId, - target_value: NonNegativeAmount, - anchor_height: BlockHeight, - exclude: &[DbT::NoteRef], - ) -> Result>, ::Error>; - - fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, AccountId>) -> usize; - - fn with_decrypted_pool_memos( - d_tx: &DecryptedTransaction<'_, AccountId>, - f: impl FnMut(&MemoBytes), - ); - - fn try_output_recovery( - params: &P, - height: BlockHeight, - tx: &Transaction, - fvk: &Self::Fvk, - ) -> Option<(Note, Address, MemoBytes)>; - - fn received_note_count(summary: &ScanSummary) -> usize; } pub(crate) fn send_single_step_proposed_transfer() { @@ -1291,7 +1225,7 @@ pub(crate) fn spend_succeeds_to_t_addr_zero_change() { ); } -pub(crate) fn change_note_spends_succeed() { +pub(crate) fn change_note_spends_succeed() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) .with_block_cache(BlockCache::new()) diff --git a/zcash_client_sqlite/src/wallet/commitment_tree.rs b/zcash_client_sqlite/src/wallet/commitment_tree.rs index 04f7412eaa..5c1d72da6b 100644 --- a/zcash_client_sqlite/src/wallet/commitment_tree.rs +++ b/zcash_client_sqlite/src/wallet/commitment_tree.rs @@ -1081,17 +1081,19 @@ 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, + }; use zcash_primitives::consensus::{BlockHeight, Network}; use super::SqliteShardStore; use crate::{ - testing::pool::ShieldedPoolTester, + testing::pool::ShieldedPoolPersistence, wallet::{init::init_wallet_db, sapling::tests::SaplingPoolTester}, WalletDb, }; - fn new_tree( + fn new_tree( m: usize, ) -> ShardTree, 4, 3> { let data_file = NamedTempFile::new().unwrap(); @@ -1191,7 +1193,7 @@ mod tests { put_shard_roots::() } - fn put_shard_roots() { + fn put_shard_roots() { let data_file = NamedTempFile::new().unwrap(); let mut db_data = WalletDb::for_path(data_file.path(), Network::TestNetwork).unwrap(); data_file.keep().unwrap(); diff --git a/zcash_client_sqlite/src/wallet/orchard.rs b/zcash_client_sqlite/src/wallet/orchard.rs index 924c6a4261..8d3d67801a 100644 --- a/zcash_client_sqlite/src/wallet/orchard.rs +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -387,18 +387,21 @@ pub(crate) fn mark_orchard_note_spent( #[cfg(test)] pub(crate) mod tests { + use std::hash::Hash; + 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, testing::TestState, DecryptedTransaction, InputSource, - WalletCommitmentTrees, WalletRead, WalletSummary, + chain::CommitmentTreeRoot, + testing::{pool::ShieldedPoolTester, TestState}, + DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, WalletSummary, }, wallet::{Note, ReceivedNote}, }; @@ -416,15 +419,17 @@ pub(crate) mod tests { }; use crate::{ - testing::{self, pool::ShieldedPoolTester}, + testing::{self, pool::ShieldedPoolPersistence}, wallet::sapling::tests::SaplingPoolTester, ORCHARD_TABLES_PREFIX, }; pub(crate) struct OrchardPoolTester; + impl ShieldedPoolPersistence for OrchardPoolTester { + const TABLES_PREFIX: &'static str = ORCHARD_TABLES_PREFIX; + } 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; @@ -491,7 +496,7 @@ pub(crate) mod tests { .put_orchard_subtree_roots(start_index, roots) } - fn next_subtree_index(s: &WalletSummary) -> u64 { + fn next_subtree_index(s: &WalletSummary) -> u64 { s.next_orchard_subtree_index() } @@ -514,14 +519,12 @@ pub(crate) mod tests { .map(|n| n.take_orchard()) } - fn decrypted_pool_outputs_count( - d_tx: &DecryptedTransaction<'_, crate::AccountId>, - ) -> usize { + fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, A>) -> usize { d_tx.orchard_outputs().len() } - fn with_decrypted_pool_memos( - d_tx: &DecryptedTransaction<'_, crate::AccountId>, + fn with_decrypted_pool_memos( + d_tx: &DecryptedTransaction<'_, A>, mut f: impl FnMut(&MemoBytes), ) { for output in d_tx.orchard_outputs() { diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs index 07ffde10a0..3b868f9a9e 100644 --- a/zcash_client_sqlite/src/wallet/sapling.rs +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -400,46 +400,49 @@ pub(crate) fn put_received_note( #[cfg(test)] pub(crate) mod tests { - use incrementalmerkletree::{Hashable, Level}; - - use shardtree::error::ShardTreeError; + use std::hash::Hash; + use incrementalmerkletree::{Hashable, Level}; use sapling::{ self, note_encryption::try_sapling_output_recovery, zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey}, }; - use zcash_primitives::{ - consensus::BlockHeight, - memo::MemoBytes, - transaction::{ - components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, - Transaction, - }, - zip32::Scope, - }; + use shardtree::error::ShardTreeError; use zcash_client_backend::{ address::Address, data_api::{ - chain::CommitmentTreeRoot, testing::TestState, DecryptedTransaction, InputSource, - WalletCommitmentTrees, WalletRead, WalletSummary, + chain::CommitmentTreeRoot, + testing::{pool::ShieldedPoolTester, TestState}, + DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, WalletSummary, }, keys::UnifiedSpendingKey, wallet::{Note, ReceivedNote}, ShieldedProtocol, }; + use zcash_primitives::{ + consensus::BlockHeight, + memo::MemoBytes, + transaction::{ + components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, + Transaction, + }, + zip32::Scope, + }; use zcash_protocol::consensus; use crate::{ - testing::{self, pool::ShieldedPoolTester}, - AccountId, SAPLING_TABLES_PREFIX, + testing::{self, pool::ShieldedPoolPersistence}, + SAPLING_TABLES_PREFIX, }; pub(crate) struct SaplingPoolTester; + impl ShieldedPoolPersistence for SaplingPoolTester { + const TABLES_PREFIX: &'static str = SAPLING_TABLES_PREFIX; + } 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; @@ -494,7 +497,7 @@ pub(crate) mod tests { .put_sapling_subtree_roots(start_index, roots) } - fn next_subtree_index(s: &WalletSummary) -> u64 { + fn next_subtree_index(s: &WalletSummary) -> u64 { s.next_sapling_subtree_index() } @@ -517,12 +520,12 @@ pub(crate) mod tests { .map(|n| n.take_sapling()) } - fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, AccountId>) -> usize { + fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, A>) -> usize { d_tx.sapling_outputs().len() } - fn with_decrypted_pool_memos( - d_tx: &DecryptedTransaction<'_, AccountId>, + fn with_decrypted_pool_memos( + d_tx: &DecryptedTransaction<'_, A>, mut f: impl FnMut(&MemoBytes), ) { for output in d_tx.sapling_outputs() { diff --git a/zcash_client_sqlite/src/wallet/scanning.rs b/zcash_client_sqlite/src/wallet/scanning.rs index 6e48069389..4916950326 100644 --- a/zcash_client_sqlite/src/wallet/scanning.rs +++ b/zcash_client_sqlite/src/wallet/scanning.rs @@ -587,7 +587,10 @@ pub(crate) mod tests { use zcash_client_backend::data_api::{ chain::{ChainState, CommitmentTreeRoot}, scanning::{spanning_tree::testing::scan_range, ScanPriority}, - testing::{AddressType, FakeCompactOutput, InitialChainState, TestBuilder, TestState}, + testing::{ + pool::ShieldedPoolTester, AddressType, FakeCompactOutput, InitialChainState, + TestBuilder, TestState, + }, AccountBirthday, Ratio, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT, }; use zcash_primitives::{ @@ -601,7 +604,6 @@ pub(crate) mod tests { error::SqliteClientError, testing::{ db::{TestDb, TestDbFactory}, - pool::ShieldedPoolTester, BlockCache, }, wallet::{ From 7e36561de80e749359f6bf443f967c87641dbcca Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 6 Sep 2024 15:06:12 -0600 Subject: [PATCH 16/20] zcash_client_backend: Move `SaplingPoolTester` here from `zcash_client_sqlite` --- zcash_client_backend/src/data_api/testing.rs | 51 +++--- .../src/data_api/testing/sapling.rs | 152 ++++++++++++++++ zcash_client_sqlite/src/chain.rs | 4 +- zcash_client_sqlite/src/testing/pool.rs | 10 +- .../src/wallet/commitment_tree.rs | 9 +- zcash_client_sqlite/src/wallet/orchard.rs | 3 +- zcash_client_sqlite/src/wallet/sapling.rs | 168 +----------------- zcash_client_sqlite/src/wallet/scanning.rs | 9 +- 8 files changed, 197 insertions(+), 209 deletions(-) create mode 100644 zcash_client_backend/src/data_api/testing/sapling.rs diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs index 9de8a3eff0..859832525e 100644 --- a/zcash_client_backend/src/data_api/testing.rs +++ b/zcash_client_backend/src/data_api/testing.rs @@ -1,4 +1,9 @@ //! Utilities for testing wallets based upon the [`crate::data_api`] traits. +use ::sapling::{ + note_encryption::{sapling_note_encryption, SaplingDomain}, + util::generate_random_rseed, + zip32::DiversifiableFullViewingKey, +}; use assert_matches::assert_matches; use core::fmt; use group::ff::Field; @@ -6,11 +11,6 @@ use incrementalmerkletree::{Marking, Retention}; use nonempty::NonEmpty; use rand::{CryptoRng, RngCore, SeedableRng}; use rand_chacha::ChaChaRng; -use sapling::{ - note_encryption::{sapling_note_encryption, SaplingDomain}, - util::generate_random_rseed, - zip32::DiversifiableFullViewingKey, -}; use secrecy::{ExposeSecret, Secret, SecretVec}; use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; use std::{ @@ -81,6 +81,7 @@ use { }; pub mod pool; +pub mod sapling; pub struct TransactionSummary { account_id: AccountId, @@ -225,7 +226,7 @@ impl CachedBlock { let sapling_final_tree = cb.vtx.iter().flat_map(|tx| tx.outputs.iter()).fold( self.chain_state.final_sapling_tree().clone(), |mut acc, c_out| { - acc.append(sapling::Node::from_cmu(&c_out.cmu().unwrap())); + acc.append(::sapling::Node::from_cmu(&c_out.cmu().unwrap())); acc }, ); @@ -552,7 +553,7 @@ where (prior_cached_block.sapling_end_size..initial_sapling_tree_size).fold( prior_cached_block.chain_state.final_sapling_tree().clone(), |mut acc, _| { - acc.append(sapling::Node::from_scalar(bls12_381::Scalar::random( + acc.append(::sapling::Node::from_scalar(bls12_381::Scalar::random( &mut self.rng, ))); acc @@ -739,7 +740,7 @@ where pub fn put_subtree_roots( &mut self, sapling_start_index: u64, - sapling_roots: &[CommitmentTreeRoot], + sapling_roots: &[CommitmentTreeRoot<::sapling::Node>], #[cfg(feature = "orchard")] orchard_start_index: u64, #[cfg(feature = "orchard")] orchard_roots: &[CommitmentTreeRoot], ) -> Result<(), ShardTreeError<::Error>> { @@ -1137,7 +1138,7 @@ fn check_proposal_serialization_roundtrip( pub struct InitialChainState { pub chain_state: ChainState, - pub prior_sapling_roots: Vec>, + pub prior_sapling_roots: Vec>, #[cfg(feature = "orchard")] pub prior_orchard_roots: Vec>, } @@ -1381,7 +1382,7 @@ impl TestBuilder { pub trait TestFvk { type Nullifier: Copy; - fn sapling_ovk(&self) -> Option; + fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey>; #[cfg(feature = "orchard")] fn orchard_ovk(&self, scope: zip32::Scope) -> Option; @@ -1426,7 +1427,7 @@ pub trait TestFvk { impl<'a, A: TestFvk> TestFvk for &'a A { type Nullifier = A::Nullifier; - fn sapling_ovk(&self) -> Option { + fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey> { (*self).sapling_ovk() } @@ -1496,7 +1497,7 @@ impl<'a, A: TestFvk> TestFvk for &'a A { impl TestFvk for DiversifiableFullViewingKey { type Nullifier = ::sapling::Nullifier; - fn sapling_ovk(&self) -> Option { + fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey> { Some(self.fvk().ovk) } @@ -1569,7 +1570,7 @@ impl TestFvk for DiversifiableFullViewingKey { impl TestFvk for orchard::keys::FullViewingKey { type Nullifier = orchard::note::Nullifier; - fn sapling_ovk(&self) -> Option { + fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey> { None } @@ -1683,15 +1684,15 @@ pub enum AddressType { fn compact_sapling_output( params: &P, height: BlockHeight, - recipient: sapling::PaymentAddress, + recipient: ::sapling::PaymentAddress, value: NonNegativeAmount, - ovk: Option, + ovk: Option<::sapling::keys::OutgoingViewingKey>, rng: &mut R, -) -> (CompactSaplingOutput, sapling::Note) { +) -> (CompactSaplingOutput, ::sapling::Note) { let rseed = generate_random_rseed(zip212_enforcement(params, height), rng); let note = ::sapling::Note::from_parts( recipient, - sapling::value::NoteValue::from_raw(value.into_u64()), + ::sapling::value::NoteValue::from_raw(value.into_u64()), rseed, ); let encryptor = sapling_note_encryption(ovk, note.clone(), *MemoBytes::empty().as_array(), rng); @@ -1995,7 +1996,7 @@ pub trait TestCache { } pub struct NoteCommitments { - sapling: Vec, + sapling: Vec<::sapling::Node>, #[cfg(feature = "orchard")] orchard: Vec, } @@ -2009,7 +2010,7 @@ impl NoteCommitments { .flat_map(|tx| { tx.outputs .iter() - .map(|out| sapling::Node::from_cmu(&out.cmu().unwrap())) + .map(|out| ::sapling::Node::from_cmu(&out.cmu().unwrap())) }) .collect(), #[cfg(feature = "orchard")] @@ -2026,7 +2027,7 @@ impl NoteCommitments { } #[allow(dead_code)] - pub fn sapling(&self) -> &[sapling::Node] { + pub fn sapling(&self) -> &[::sapling::Node] { self.sapling.as_ref() } @@ -2039,7 +2040,7 @@ impl NoteCommitments { pub struct MockWalletDb { pub network: Network, pub sapling_tree: ShardTree< - MemoryShardStore, + MemoryShardStore<::sapling::Node, BlockHeight>, { SAPLING_SHARD_HEIGHT * 2 }, SAPLING_SHARD_HEIGHT, >, @@ -2216,7 +2217,7 @@ impl WalletRead for MockWalletDb { fn get_sapling_nullifiers( &self, _query: NullifierQuery, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { Ok(Vec::new()) } @@ -2375,14 +2376,14 @@ impl WalletWrite for MockWalletDb { impl WalletCommitmentTrees for MockWalletDb { type Error = Infallible; - type SaplingShardStore<'a> = MemoryShardStore; + type SaplingShardStore<'a> = MemoryShardStore<::sapling::Node, BlockHeight>; fn with_sapling_tree_mut(&mut self, mut callback: F) -> Result where for<'a> F: FnMut( &'a mut ShardTree< Self::SaplingShardStore<'a>, - { sapling::NOTE_COMMITMENT_TREE_DEPTH }, + { ::sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT, >, ) -> Result, @@ -2394,7 +2395,7 @@ impl WalletCommitmentTrees for MockWalletDb { fn put_sapling_subtree_roots( &mut self, start_index: u64, - roots: &[CommitmentTreeRoot], + roots: &[CommitmentTreeRoot<::sapling::Node>], ) -> Result<(), ShardTreeError> { self.with_sapling_tree_mut(|t| { for (root, i) in roots.iter().zip(0u64..) { diff --git a/zcash_client_backend/src/data_api/testing/sapling.rs b/zcash_client_backend/src/data_api/testing/sapling.rs new file mode 100644 index 0000000000..6f9a78fb42 --- /dev/null +++ b/zcash_client_backend/src/data_api/testing/sapling.rs @@ -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( + st: &TestState, + ) -> 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( + st: &mut TestState, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError<::Error>> { + st.wallet_mut() + .put_sapling_subtree_roots(start_index, roots) + } + + fn next_subtree_index(s: &WalletSummary) -> u64 { + s.next_sapling_subtree_index() + } + + fn select_spendable_notes( + st: &TestState, + account: ::AccountId, + target_value: Zatoshis, + anchor_height: BlockHeight, + exclude: &[DbT::NoteRef], + ) -> Result>, ::Error> { + st.wallet() + .select_spendable_notes( + account, + target_value, + &[ShieldedProtocol::Sapling], + anchor_height, + exclude, + ) + .map(|n| n.take_sapling()) + } + + fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, A>) -> usize { + d_tx.sapling_outputs().len() + } + + fn with_decrypted_pool_memos( + d_tx: &DecryptedTransaction<'_, A>, + mut f: impl FnMut(&MemoBytes), + ) { + for output in d_tx.sapling_outputs() { + f(output.memo()); + } + } + + fn try_output_recovery( + 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() + } +} diff --git a/zcash_client_sqlite/src/chain.rs b/zcash_client_sqlite/src/chain.rs index 028b2980f3..e9174381fa 100644 --- a/zcash_client_sqlite/src/chain.rs +++ b/zcash_client_sqlite/src/chain.rs @@ -322,7 +322,9 @@ 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; diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index a76f409ddf..22e517c855 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -34,8 +34,8 @@ use zcash_client_backend::{ chain::{self, ChainState, CommitmentTreeRoot}, error::Error, testing::{ - input_selector, pool::ShieldedPoolTester, AddressType, FakeCompactOutput, - InitialChainState, TestBuilder, TestState, + input_selector, pool::ShieldedPoolTester, sapling::SaplingPoolTester, AddressType, + FakeCompactOutput, InitialChainState, TestBuilder, TestState, }, wallet::{ decrypt_and_store_transaction, @@ -58,7 +58,7 @@ use crate::{ BlockCache, }, wallet::{commitment_tree, parse_scope, truncate_to_height}, - NoteId, ReceivedNoteId, + NoteId, ReceivedNoteId, SAPLING_TABLES_PREFIX, }; #[cfg(feature = "transparent-inputs")] @@ -81,6 +81,10 @@ pub(crate) trait ShieldedPoolPersistence { const TABLES_PREFIX: &'static str; } +impl ShieldedPoolPersistence for SaplingPoolTester { + const TABLES_PREFIX: &'static str = SAPLING_TABLES_PREFIX; +} + pub(crate) fn send_single_step_proposed_transfer() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) diff --git a/zcash_client_sqlite/src/wallet/commitment_tree.rs b/zcash_client_sqlite/src/wallet/commitment_tree.rs index 5c1d72da6b..2ca54b9ed3 100644 --- a/zcash_client_sqlite/src/wallet/commitment_tree.rs +++ b/zcash_client_sqlite/src/wallet/commitment_tree.rs @@ -1082,16 +1082,13 @@ mod tests { }; use shardtree::ShardTree; use zcash_client_backend::data_api::{ - chain::CommitmentTreeRoot, testing::pool::ShieldedPoolTester, + chain::CommitmentTreeRoot, + testing::{pool::ShieldedPoolTester, sapling::SaplingPoolTester}, }; use zcash_primitives::consensus::{BlockHeight, Network}; use super::SqliteShardStore; - use crate::{ - testing::pool::ShieldedPoolPersistence, - wallet::{init::init_wallet_db, sapling::tests::SaplingPoolTester}, - WalletDb, - }; + use crate::{testing::pool::ShieldedPoolPersistence, wallet::init::init_wallet_db, WalletDb}; fn new_tree( m: usize, diff --git a/zcash_client_sqlite/src/wallet/orchard.rs b/zcash_client_sqlite/src/wallet/orchard.rs index 8d3d67801a..67ca0da660 100644 --- a/zcash_client_sqlite/src/wallet/orchard.rs +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -400,7 +400,7 @@ pub(crate) mod tests { use zcash_client_backend::{ data_api::{ chain::CommitmentTreeRoot, - testing::{pool::ShieldedPoolTester, TestState}, + testing::{pool::ShieldedPoolTester, sapling::SaplingPoolTester, TestState}, DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, WalletSummary, }, wallet::{Note, ReceivedNote}, @@ -420,7 +420,6 @@ pub(crate) mod tests { use crate::{ testing::{self, pool::ShieldedPoolPersistence}, - wallet::sapling::tests::SaplingPoolTester, ORCHARD_TABLES_PREFIX, }; diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs index 3b868f9a9e..9b60305b66 100644 --- a/zcash_client_sqlite/src/wallet/sapling.rs +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -400,173 +400,9 @@ pub(crate) fn put_received_note( #[cfg(test)] pub(crate) mod tests { - use std::hash::Hash; + use zcash_client_backend::data_api::testing::sapling::SaplingPoolTester; - use incrementalmerkletree::{Hashable, Level}; - use sapling::{ - self, - note_encryption::try_sapling_output_recovery, - zip32::{DiversifiableFullViewingKey, ExtendedSpendingKey}, - }; - use shardtree::error::ShardTreeError; - - use zcash_client_backend::{ - address::Address, - data_api::{ - chain::CommitmentTreeRoot, - testing::{pool::ShieldedPoolTester, TestState}, - DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, WalletSummary, - }, - keys::UnifiedSpendingKey, - wallet::{Note, ReceivedNote}, - ShieldedProtocol, - }; - use zcash_primitives::{ - consensus::BlockHeight, - memo::MemoBytes, - transaction::{ - components::{amount::NonNegativeAmount, sapling::zip212_enforcement}, - Transaction, - }, - zip32::Scope, - }; - use zcash_protocol::consensus; - - use crate::{ - testing::{self, pool::ShieldedPoolPersistence}, - SAPLING_TABLES_PREFIX, - }; - - pub(crate) struct SaplingPoolTester; - impl ShieldedPoolPersistence for SaplingPoolTester { - const TABLES_PREFIX: &'static str = SAPLING_TABLES_PREFIX; - } - 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( - st: &TestState, - ) -> 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( - st: &mut TestState, - start_index: u64, - roots: &[CommitmentTreeRoot], - ) -> Result<(), ShardTreeError<::Error>> { - st.wallet_mut() - .put_sapling_subtree_roots(start_index, roots) - } - - fn next_subtree_index(s: &WalletSummary) -> u64 { - s.next_sapling_subtree_index() - } - - fn select_spendable_notes( - st: &TestState, - account: ::AccountId, - target_value: NonNegativeAmount, - anchor_height: BlockHeight, - exclude: &[DbT::NoteRef], - ) -> Result>, ::Error> - { - st.wallet() - .select_spendable_notes( - account, - target_value, - &[ShieldedProtocol::Sapling], - anchor_height, - exclude, - ) - .map(|n| n.take_sapling()) - } - - fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, A>) -> usize { - d_tx.sapling_outputs().len() - } - - fn with_decrypted_pool_memos( - d_tx: &DecryptedTransaction<'_, A>, - mut f: impl FnMut(&MemoBytes), - ) { - for output in d_tx.sapling_outputs() { - f(output.memo()); - } - } - - fn try_output_recovery( - 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: &zcash_client_backend::data_api::chain::ScanSummary, - ) -> usize { - summary.received_sapling_note_count() - } - } + use crate::testing; #[test] fn send_single_step_proposed_transfer() { diff --git a/zcash_client_sqlite/src/wallet/scanning.rs b/zcash_client_sqlite/src/wallet/scanning.rs index 4916950326..94da3fca1f 100644 --- a/zcash_client_sqlite/src/wallet/scanning.rs +++ b/zcash_client_sqlite/src/wallet/scanning.rs @@ -588,8 +588,8 @@ pub(crate) mod tests { chain::{ChainState, CommitmentTreeRoot}, scanning::{spanning_tree::testing::scan_range, ScanPriority}, testing::{ - pool::ShieldedPoolTester, AddressType, FakeCompactOutput, InitialChainState, - TestBuilder, TestState, + pool::ShieldedPoolTester, sapling::SaplingPoolTester, AddressType, FakeCompactOutput, + InitialChainState, TestBuilder, TestState, }, AccountBirthday, Ratio, WalletRead, WalletWrite, SAPLING_SHARD_HEIGHT, }; @@ -606,10 +606,7 @@ pub(crate) mod tests { db::{TestDb, TestDbFactory}, BlockCache, }, - wallet::{ - sapling::tests::SaplingPoolTester, - scanning::{insert_queue_entries, replace_queue_entries, suggest_scan_ranges}, - }, + wallet::scanning::{insert_queue_entries, replace_queue_entries, suggest_scan_ranges}, VERIFY_LOOKAHEAD, }; From 33b8f89a6a52142408864722c548a309dac6bde4 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 6 Sep 2024 15:19:20 -0600 Subject: [PATCH 17/20] zcash_client_backend: Move `OrchardPoolTester` here from `zcash_client_sqlite` --- zcash_client_backend/src/data_api/testing.rs | 51 ++--- .../src/data_api/testing/orchard.rs | 172 +++++++++++++++++ zcash_client_sqlite/src/chain.rs | 2 +- zcash_client_sqlite/src/testing/pool.rs | 8 +- .../src/wallet/commitment_tree.rs | 2 +- zcash_client_sqlite/src/wallet/orchard.rs | 182 +----------------- zcash_client_sqlite/src/wallet/sapling.rs | 13 +- zcash_client_sqlite/src/wallet/scanning.rs | 6 +- 8 files changed, 218 insertions(+), 218 deletions(-) create mode 100644 zcash_client_backend/src/data_api/testing/orchard.rs diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs index 859832525e..9fca64011b 100644 --- a/zcash_client_backend/src/data_api/testing.rs +++ b/zcash_client_backend/src/data_api/testing.rs @@ -77,9 +77,11 @@ use { #[cfg(feature = "orchard")] use { super::ORCHARD_SHARD_HEIGHT, crate::proto::compact_formats::CompactOrchardAction, - group::ff::PrimeField, orchard::tree::MerkleHashOrchard, pasta_curves::pallas, + ::orchard::tree::MerkleHashOrchard, group::ff::PrimeField, pasta_curves::pallas, }; +#[cfg(feature = "orchard")] +pub mod orchard; pub mod pool; pub mod sapling; @@ -396,7 +398,7 @@ where /// Returns the test account's Orchard FVK, if one was configured. #[cfg(feature = "orchard")] - pub fn test_account_orchard(&self) -> Option<&orchard::keys::FullViewingKey> { + pub fn test_account_orchard(&self) -> Option<&::orchard::keys::FullViewingKey> { let (_, acct) = self.test_account.as_ref()?; let ufvk = acct.ufvk()?; ufvk.orchard() @@ -1385,7 +1387,7 @@ pub trait TestFvk { fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey>; #[cfg(feature = "orchard")] - fn orchard_ovk(&self, scope: zip32::Scope) -> Option; + fn orchard_ovk(&self, scope: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey>; fn add_spend( &self, @@ -1432,7 +1434,7 @@ impl<'a, A: TestFvk> TestFvk for &'a A { } #[cfg(feature = "orchard")] - fn orchard_ovk(&self, scope: zip32::Scope) -> Option { + fn orchard_ovk(&self, scope: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey> { (*self).orchard_ovk(scope) } @@ -1502,7 +1504,7 @@ impl TestFvk for DiversifiableFullViewingKey { } #[cfg(feature = "orchard")] - fn orchard_ovk(&self, _: zip32::Scope) -> Option { + fn orchard_ovk(&self, _: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey> { None } @@ -1567,14 +1569,14 @@ impl TestFvk for DiversifiableFullViewingKey { } #[cfg(feature = "orchard")] -impl TestFvk for orchard::keys::FullViewingKey { - type Nullifier = orchard::note::Nullifier; +impl TestFvk for ::orchard::keys::FullViewingKey { + type Nullifier = ::orchard::note::Nullifier; fn sapling_ovk(&self) -> Option<::sapling::keys::OutgoingViewingKey> { None } - fn orchard_ovk(&self, scope: zip32::Scope) -> Option { + fn orchard_ovk(&self, scope: zip32::Scope) -> Option<::orchard::keys::OutgoingViewingKey> { Some(self.to_ovk(scope)) } @@ -1588,9 +1590,9 @@ impl TestFvk for orchard::keys::FullViewingKey { let recipient = loop { let mut bytes = [0; 32]; rng.fill_bytes(&mut bytes); - let sk = orchard::keys::SpendingKey::from_bytes(bytes); + let sk = ::orchard::keys::SpendingKey::from_bytes(bytes); if sk.is_some().into() { - break orchard::keys::FullViewingKey::from(&sk.unwrap()) + break ::orchard::keys::FullViewingKey::from(&sk.unwrap()) .address_at(0u32, zip32::Scope::External); } }; @@ -1617,7 +1619,7 @@ impl TestFvk for orchard::keys::FullViewingKey { ) -> Self::Nullifier { // Generate a dummy nullifier for the spend let revealed_spent_note_nullifier = - orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr()) + ::orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr()) .unwrap(); let (j, scope) = match req { @@ -1715,19 +1717,19 @@ fn compact_sapling_output( /// Returns the `CompactOrchardAction` and the new note. #[cfg(feature = "orchard")] fn compact_orchard_action( - nf_old: orchard::note::Nullifier, - recipient: orchard::Address, + nf_old: ::orchard::note::Nullifier, + recipient: ::orchard::Address, value: NonNegativeAmount, - ovk: Option, + ovk: Option<::orchard::keys::OutgoingViewingKey>, rng: &mut R, -) -> (CompactOrchardAction, orchard::Note) { +) -> (CompactOrchardAction, ::orchard::Note) { use zcash_note_encryption::ShieldedOutput; - let (compact_action, note) = orchard::note_encryption::testing::fake_compact_action( + let (compact_action, note) = ::orchard::note_encryption::testing::fake_compact_action( rng, nf_old, recipient, - orchard::value::NoteValue::from_raw(value.into_u64()), + ::orchard::value::NoteValue::from_raw(value.into_u64()), ovk, ); @@ -1906,9 +1908,10 @@ fn fake_compact_block_spending( #[cfg(feature = "orchard")] if let Some(recipient) = ua.orchard() { // Generate a dummy nullifier - let nullifier = - orchard::note::Nullifier::from_bytes(&pallas::Base::random(&mut rng).to_repr()) - .unwrap(); + let nullifier = ::orchard::note::Nullifier::from_bytes( + &pallas::Base::random(&mut rng).to_repr(), + ) + .unwrap(); ctx.actions.push( compact_orchard_action( @@ -2046,7 +2049,7 @@ pub struct MockWalletDb { >, #[cfg(feature = "orchard")] pub orchard_tree: ShardTree< - MemoryShardStore, + MemoryShardStore<::orchard::tree::MerkleHashOrchard, BlockHeight>, { ORCHARD_SHARD_HEIGHT * 2 }, ORCHARD_SHARD_HEIGHT, >, @@ -2225,7 +2228,7 @@ impl WalletRead for MockWalletDb { fn get_orchard_nullifiers( &self, _query: NullifierQuery, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { Ok(Vec::new()) } @@ -2412,7 +2415,7 @@ impl WalletCommitmentTrees for MockWalletDb { } #[cfg(feature = "orchard")] - type OrchardShardStore<'a> = MemoryShardStore; + type OrchardShardStore<'a> = MemoryShardStore<::orchard::tree::MerkleHashOrchard, BlockHeight>; #[cfg(feature = "orchard")] fn with_orchard_tree_mut(&mut self, mut callback: F) -> Result @@ -2434,7 +2437,7 @@ impl WalletCommitmentTrees for MockWalletDb { fn put_orchard_subtree_roots( &mut self, start_index: u64, - roots: &[CommitmentTreeRoot], + roots: &[CommitmentTreeRoot<::orchard::tree::MerkleHashOrchard>], ) -> Result<(), ShardTreeError> { self.with_orchard_tree_mut(|t| { for (root, i) in roots.iter().zip(0u64..) { diff --git a/zcash_client_backend/src/data_api/testing/orchard.rs b/zcash_client_backend/src/data_api/testing/orchard.rs new file mode 100644 index 0000000000..6c8ef143d8 --- /dev/null +++ b/zcash_client_backend/src/data_api/testing/orchard.rs @@ -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( + st: &TestState, + ) -> 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( + st: &mut TestState, + start_index: u64, + roots: &[CommitmentTreeRoot], + ) -> Result<(), ShardTreeError<::Error>> { + st.wallet_mut() + .put_orchard_subtree_roots(start_index, roots) + } + + fn next_subtree_index(s: &WalletSummary) -> u64 { + s.next_orchard_subtree_index() + } + + fn select_spendable_notes( + st: &TestState, + account: ::AccountId, + target_value: Zatoshis, + anchor_height: BlockHeight, + exclude: &[DbT::NoteRef], + ) -> Result>, ::Error> { + st.wallet() + .select_spendable_notes( + account, + target_value, + &[ShieldedProtocol::Orchard], + anchor_height, + exclude, + ) + .map(|n| n.take_orchard()) + } + + fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, A>) -> usize { + d_tx.orchard_outputs().len() + } + + fn with_decrypted_pool_memos( + d_tx: &DecryptedTransaction<'_, A>, + mut f: impl FnMut(&MemoBytes), + ) { + for output in d_tx.orchard_outputs() { + f(output.memo()); + } + } + + fn try_output_recovery( + _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() + } +} diff --git a/zcash_client_sqlite/src/chain.rs b/zcash_client_sqlite/src/chain.rs index e9174381fa..88951daa07 100644 --- a/zcash_client_sqlite/src/chain.rs +++ b/zcash_client_sqlite/src/chain.rs @@ -327,7 +327,7 @@ mod tests { 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() { diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index 22e517c855..182f0d4b89 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -73,7 +73,8 @@ use { #[cfg(feature = "orchard")] use { - zcash_client_backend::PoolType, + crate::ORCHARD_TABLES_PREFIX, + zcash_client_backend::{data_api::testing::orchard::OrchardPoolTester, PoolType}, zcash_protocol::{consensus::BlockHeight, ShieldedProtocol}, }; @@ -85,6 +86,11 @@ impl ShieldedPoolPersistence for SaplingPoolTester { const TABLES_PREFIX: &'static str = SAPLING_TABLES_PREFIX; } +#[cfg(feature = "orchard")] +impl ShieldedPoolPersistence for OrchardPoolTester { + const TABLES_PREFIX: &'static str = ORCHARD_TABLES_PREFIX; +} + pub(crate) fn send_single_step_proposed_transfer() { let mut st = TestBuilder::new() .with_data_store_factory(TestDbFactory) diff --git a/zcash_client_sqlite/src/wallet/commitment_tree.rs b/zcash_client_sqlite/src/wallet/commitment_tree.rs index 2ca54b9ed3..38c2eb0243 100644 --- a/zcash_client_sqlite/src/wallet/commitment_tree.rs +++ b/zcash_client_sqlite/src/wallet/commitment_tree.rs @@ -1107,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() { diff --git a/zcash_client_sqlite/src/wallet/orchard.rs b/zcash_client_sqlite/src/wallet/orchard.rs index 67ca0da660..ec3b5f468b 100644 --- a/zcash_client_sqlite/src/wallet/orchard.rs +++ b/zcash_client_sqlite/src/wallet/orchard.rs @@ -387,188 +387,12 @@ pub(crate) fn mark_orchard_note_spent( #[cfg(test)] pub(crate) mod tests { - use std::hash::Hash; - use incrementalmerkletree::{Hashable, Level}; - use orchard::{ - keys::{FullViewingKey, SpendingKey}, - note_encryption::OrchardDomain, - tree::MerkleHashOrchard, + use zcash_client_backend::data_api::testing::{ + orchard::OrchardPoolTester, sapling::SaplingPoolTester, }; - use shardtree::error::ShardTreeError; - - use zcash_client_backend::{ - data_api::{ - chain::CommitmentTreeRoot, - testing::{pool::ShieldedPoolTester, sapling::SaplingPoolTester, TestState}, - DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, 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::{self, BlockHeight}, - memo::MemoBytes, - value::Zatoshis, - ShieldedProtocol, - }; - - use crate::{ - testing::{self, pool::ShieldedPoolPersistence}, - ORCHARD_TABLES_PREFIX, - }; - - pub(crate) struct OrchardPoolTester; - impl ShieldedPoolPersistence for OrchardPoolTester { - const TABLES_PREFIX: &'static str = ORCHARD_TABLES_PREFIX; - } - 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( - st: &TestState, - ) -> 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( - st: &mut TestState, - start_index: u64, - roots: &[CommitmentTreeRoot], - ) -> Result<(), ShardTreeError<::Error>> { - st.wallet_mut() - .put_orchard_subtree_roots(start_index, roots) - } - - fn next_subtree_index(s: &WalletSummary) -> u64 { - s.next_orchard_subtree_index() - } - - fn select_spendable_notes( - st: &TestState, - account: ::AccountId, - target_value: Zatoshis, - anchor_height: BlockHeight, - exclude: &[DbT::NoteRef], - ) -> Result>, ::Error> - { - st.wallet() - .select_spendable_notes( - account, - target_value, - &[ShieldedProtocol::Orchard], - anchor_height, - exclude, - ) - .map(|n| n.take_orchard()) - } - - fn decrypted_pool_outputs_count(d_tx: &DecryptedTransaction<'_, A>) -> usize { - d_tx.orchard_outputs().len() - } - - fn with_decrypted_pool_memos( - d_tx: &DecryptedTransaction<'_, A>, - mut f: impl FnMut(&MemoBytes), - ) { - for output in d_tx.orchard_outputs() { - f(output.memo()); - } - } - - fn try_output_recovery( - _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: &zcash_client_backend::data_api::chain::ScanSummary, - ) -> usize { - summary.received_orchard_note_count() - } - } + use crate::testing::{self}; #[test] fn send_single_step_proposed_transfer() { diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs index 9b60305b66..67ed843d7c 100644 --- a/zcash_client_sqlite/src/wallet/sapling.rs +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -404,6 +404,9 @@ pub(crate) mod tests { use crate::testing; + #[cfg(feature = "orchard")] + use zcash_client_backend::data_api::testing::orchard::OrchardPoolTester; + #[test] fn send_single_step_proposed_transfer() { testing::pool::send_single_step_proposed_transfer::() @@ -496,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::() } #[test] #[cfg(feature = "orchard")] fn fully_funded_fully_private() { - use crate::wallet::orchard::tests::OrchardPoolTester; - testing::pool::fully_funded_fully_private::() } #[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::() } #[test] #[cfg(feature = "orchard")] fn multi_pool_checkpoint() { - use crate::wallet::orchard::tests::OrchardPoolTester; - testing::pool::multi_pool_checkpoint::() } #[test] #[cfg(feature = "orchard")] fn multi_pool_checkpoints_with_pruning() { - use crate::wallet::orchard::tests::OrchardPoolTester; - testing::pool::multi_pool_checkpoints_with_pruning::() } } diff --git a/zcash_client_sqlite/src/wallet/scanning.rs b/zcash_client_sqlite/src/wallet/scanning.rs index 94da3fca1f..9d9bcc78c0 100644 --- a/zcash_client_sqlite/src/wallet/scanning.rs +++ b/zcash_client_sqlite/src/wallet/scanning.rs @@ -612,12 +612,14 @@ pub(crate) mod tests { #[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, }, From d4e26d5e4bb1979bbc1f074e19fa08801377436c Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Fri, 6 Sep 2024 17:56:20 -0600 Subject: [PATCH 18/20] zcash_client_backend: Migrate `send_single_step_proposed_transfer` test from `zcash_client_sqlite` --- zcash_client_backend/src/data_api.rs | 16 +- zcash_client_backend/src/data_api/testing.rs | 30 +-- .../src/data_api/testing/pool.rs | 164 +++++++++++++- zcash_client_sqlite/src/lib.rs | 27 ++- zcash_client_sqlite/src/testing.rs | 19 +- zcash_client_sqlite/src/testing/db.rs | 4 +- zcash_client_sqlite/src/testing/pool.rs | 204 +++--------------- 7 files changed, 255 insertions(+), 209 deletions(-) diff --git a/zcash_client_backend/src/data_api.rs b/zcash_client_backend/src/data_api.rs index 6f1ee1a934..2f361ca486 100644 --- a/zcash_client_backend/src/data_api.rs +++ b/zcash_client_backend/src/data_api.rs @@ -102,8 +102,11 @@ use { zcash_primitives::legacy::TransparentAddress, }; +#[cfg(feature = "test-dependencies")] +use ambassador::delegatable_trait; + #[cfg(any(test, feature = "test-dependencies"))] -use {ambassador::delegatable_trait, zcash_primitives::consensus::NetworkUpgrade}; +use zcash_primitives::consensus::NetworkUpgrade; pub mod chain; pub mod error; @@ -1175,6 +1178,17 @@ pub trait WalletRead { ) -> Result>, 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, Self::Error> { + Ok(vec![]) + } } /// The relevance of a seed to a given wallet. diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs index 9fca64011b..9f85cd4505 100644 --- a/zcash_client_backend/src/data_api/testing.rs +++ b/zcash_client_backend/src/data_api/testing.rs @@ -16,20 +16,13 @@ use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTre use std::{ collections::{BTreeMap, HashMap}, convert::Infallible, + hash::Hash, num::NonZeroU32, }; use subtle::ConditionallySelectable; + use zcash_keys::address::Address; use zcash_note_encryption::Domain; -use zcash_proofs::prover::LocalTxProver; -use zcash_protocol::{ - consensus::{self, NetworkUpgrade, Parameters as _}, - local_consensus::LocalNetwork, - memo::MemoBytes, - value::{ZatBalance, Zatoshis}, -}; -use zip32::{fingerprint::SeedFingerprint, DiversifierIndex}; - use zcash_primitives::{ block::BlockHash, consensus::{BlockHeight, Network}, @@ -40,6 +33,14 @@ use zcash_primitives::{ Transaction, TxId, }, }; +use zcash_proofs::prover::LocalTxProver; +use zcash_protocol::{ + consensus::{self, NetworkUpgrade, Parameters as _}, + local_consensus::LocalNetwork, + memo::MemoBytes, + value::{ZatBalance, Zatoshis}, +}; +use zip32::{fingerprint::SeedFingerprint, DiversifierIndex}; use crate::{ address::UnifiedAddress, @@ -1147,9 +1148,11 @@ pub struct InitialChainState { pub trait DataStoreFactory { type Error: core::fmt::Debug; - type AccountId: ConditionallySelectable + Default + Send + 'static; - type DataStore: InputSource - + WalletRead + type AccountId: ConditionallySelectable + Default + Hash + Eq + Send + 'static; + type Account: Account + Clone; + type DsError: core::fmt::Debug; + type DataStore: InputSource + + WalletRead + WalletWrite + WalletCommitmentTrees; @@ -1988,7 +1991,8 @@ fn fake_compact_block_from_compact_tx( /// Trait used by tests that require a block cache. pub trait TestCache { - type BlockSource: BlockSource; + type BsError: core::fmt::Debug; + type BlockSource: BlockSource; type InsertResult; /// Exposes the block cache as a [`BlockSource`]. diff --git a/zcash_client_backend/src/data_api/testing/pool.rs b/zcash_client_backend/src/data_api/testing/pool.rs index 3e99306c33..fb7b838057 100644 --- a/zcash_client_backend/src/data_api/testing/pool.rs +++ b/zcash_client_backend/src/data_api/testing/pool.rs @@ -1,26 +1,36 @@ -use std::{cmp::Eq, hash::Hash}; - +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::transaction::Transaction; +use zcash_primitives::{ + block::BlockHash, + transaction::{fees::StandardFeeRule, Transaction}, +}; use zcash_protocol::{ consensus::{self, BlockHeight}, - memo::MemoBytes, + memo::{Memo, MemoBytes}, value::Zatoshis, ShieldedProtocol, }; +use zip321::Payment; use crate::{ data_api::{ chain::{CommitmentTreeRoot, ScanSummary}, - DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, WalletSummary, + testing::{AddressType, TestBuilder}, + wallet::{decrypt_and_store_transaction, input_selection::GreedyInputSelector}, + Account as _, DecryptedTransaction, InputSource, WalletCommitmentTrees, WalletRead, + WalletSummary, }, - wallet::{Note, ReceivedNote}, + decrypt_transaction, + fees::{standard, DustOutputPolicy}, + wallet::{Note, NoteId, OvkPolicy, ReceivedNote}, }; -use super::{TestFvk, TestState}; +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. @@ -88,3 +98,143 @@ pub trait ShieldedPoolTester { fn received_note_count(summary: &ScanSummary) -> usize; } + +pub fn send_single_step_proposed_transfer( + 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::().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::( + 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(_) + ); +} diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 9091fe4a3a..1c69594467 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -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")] @@ -619,6 +619,31 @@ impl, P: consensus::Parameters> WalletRead for W 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, + protocol: ShieldedProtocol, + ) -> Result, 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::, _>>() + .map_err(SqliteClientError::from) + } } impl WalletWrite for WalletDb { diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 2aaafb6766..42c43da87d 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -1,25 +1,24 @@ use prost::Message; - use rusqlite::params; - use tempfile::NamedTempFile; -#[cfg(feature = "unstable")] -use {std::fs::File, tempfile::TempDir}; - use zcash_client_backend::data_api::testing::{NoteCommitments, TestCache}; #[allow(deprecated)] use zcash_client_backend::proto::compact_formats::CompactBlock; -use crate::chain::init::init_cache_database; +use crate::{chain::init::init_cache_database, error::SqliteClientError}; use super::BlockDb; #[cfg(feature = "unstable")] -use crate::{ - chain::{init::init_blockmeta_db, BlockMeta}, - FsBlockDb, +use { + crate::{ + chain::{init::init_blockmeta_db, BlockMeta}, + FsBlockDb, FsBlockDbError, + }, + std::fs::File, + tempfile::TempDir, }; pub(crate) mod db; @@ -44,6 +43,7 @@ impl BlockCache { } impl TestCache for BlockCache { + type BsError = SqliteClientError; type BlockSource = BlockDb; type InsertResult = NoteCommitments; @@ -86,6 +86,7 @@ impl FsBlockCache { #[cfg(feature = "unstable")] impl TestCache for FsBlockCache { + type BsError = FsBlockDbError; type BlockSource = FsBlockDb; type InsertResult = BlockMeta; diff --git a/zcash_client_sqlite/src/testing/db.rs b/zcash_client_sqlite/src/testing/db.rs index 745255d206..983e875798 100644 --- a/zcash_client_sqlite/src/testing/db.rs +++ b/zcash_client_sqlite/src/testing/db.rs @@ -31,7 +31,7 @@ use zcash_primitives::{ }; use zcash_protocol::{consensus::BlockHeight, local_consensus::LocalNetwork, memo::Memo}; -use crate::{wallet::init::init_wallet_db, AccountId, WalletDb}; +use crate::{error::SqliteClientError, wallet::init::init_wallet_db, AccountId, WalletDb}; #[cfg(feature = "transparent-inputs")] use { @@ -149,6 +149,8 @@ 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 { diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index 182f0d4b89..795d49f03a 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -12,21 +12,6 @@ use incrementalmerkletree::frontier::Frontier; use rusqlite::params; use secrecy::Secret; -use zcash_primitives::{ - block::BlockHash, - consensus::{BranchId, NetworkUpgrade, Parameters}, - legacy::TransparentAddress, - memo::{Memo, MemoBytes}, - transaction::{ - components::amount::NonNegativeAmount, - fees::{ - fixed::FeeRule as FixedFeeRule, zip317::FeeError as Zip317FeeError, StandardFeeRule, - }, - Transaction, - }, - zip32::Scope, -}; - use zcash_client_backend::{ address::Address, data_api::{ @@ -37,19 +22,29 @@ use zcash_client_backend::{ input_selector, pool::ShieldedPoolTester, sapling::SaplingPoolTester, AddressType, FakeCompactOutput, InitialChainState, TestBuilder, TestState, }, - wallet::{ - decrypt_and_store_transaction, - input_selection::{GreedyInputSelector, GreedyInputSelectorError}, - }, + wallet::input_selection::{GreedyInputSelector, GreedyInputSelectorError}, Account as _, AccountBirthday, Ratio, WalletRead, WalletWrite, }, - decrypt_transaction, fees::{fixed, standard, DustOutputPolicy}, keys::UnifiedSpendingKey, scanning::ScanError, wallet::{Note, OvkPolicy}, - zip321::{self, Payment, TransactionRequest}, + zip321::{Payment, TransactionRequest}, +}; +use zcash_primitives::{ + block::BlockHash, + consensus::{BranchId, NetworkUpgrade, Parameters}, + legacy::TransparentAddress, + transaction::{ + components::amount::NonNegativeAmount, + fees::{ + fixed::FeeRule as FixedFeeRule, zip317::FeeError as Zip317FeeError, StandardFeeRule, + }, + Transaction, + }, + zip32::Scope, }; +use zcash_protocol::memo::MemoBytes; use crate::{ error::SqliteClientError, @@ -58,7 +53,7 @@ use crate::{ BlockCache, }, wallet::{commitment_tree, parse_scope, truncate_to_height}, - NoteId, ReceivedNoteId, SAPLING_TABLES_PREFIX, + ReceivedNoteId, SAPLING_TABLES_PREFIX, }; #[cfg(feature = "transparent-inputs")] @@ -69,6 +64,7 @@ use { components::{OutPoint, TxOut}, fees::zip317, }, + zcash_protocol::memo::Memo, }; #[cfg(feature = "orchard")] @@ -92,156 +88,10 @@ impl ShieldedPoolPersistence for OrchardPoolTester { } pub(crate) fn send_single_step_proposed_transfer() { - let mut st = TestBuilder::new() - .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 dfvk = T::test_account_fvk(&st); - - // Add funds to the wallet in a single note - let value = NonNegativeAmount::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()), - NonNegativeAmount::const_from_u64(10000), - )]) - .unwrap(); - - let fee_rule = StandardFeeRule::Zip317; - - let change_memo = "Test change memo".parse::().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::( - 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 = { - let mut stmt_sent_notes = st - .wallet() - .conn() - .prepare( - "SELECT output_index - FROM sent_notes - JOIN transactions ON transactions.id_tx = sent_notes.tx - WHERE transactions.txid = ?", - ) - .unwrap(); - - stmt_sent_notes - .query(rusqlite::params![sent_tx_id.as_ref()]) - .unwrap() - .mapped(|row| Ok(NoteId::new(sent_tx_id, T::SHIELDED_PROTOCOL, row.get(0)?))) - .collect::, _>>() - .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(_) - ); + zcash_client_backend::data_api::testing::pool::send_single_step_proposed_transfer::( + TestDbFactory, + BlockCache::new(), + ) } #[cfg(feature = "transparent-inputs")] @@ -1755,7 +1605,7 @@ pub(crate) fn pool_crossing_required Date: Tue, 10 Sep 2024 15:14:23 +0000 Subject: [PATCH 19/20] Address non-documentation review comments --- zcash_client_backend/src/data_api/testing.rs | 24 +++++++--------- zcash_client_sqlite/src/lib.rs | 29 +++++++++++++++----- zcash_client_sqlite/src/testing.rs | 4 +-- zcash_client_sqlite/src/testing/db.rs | 5 +--- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs index 9f85cd4505..a32e48f380 100644 --- a/zcash_client_backend/src/data_api/testing.rs +++ b/zcash_client_backend/src/data_api/testing.rs @@ -1,11 +1,19 @@ //! Utilities for testing wallets based upon the [`crate::data_api`] traits. + +use std::{ + collections::{BTreeMap, HashMap}, + convert::Infallible, + fmt, + hash::Hash, + num::NonZeroU32, +}; + use ::sapling::{ note_encryption::{sapling_note_encryption, SaplingDomain}, util::generate_random_rseed, zip32::DiversifiableFullViewingKey, }; use assert_matches::assert_matches; -use core::fmt; use group::ff::Field; use incrementalmerkletree::{Marking, Retention}; use nonempty::NonEmpty; @@ -13,12 +21,6 @@ use rand::{CryptoRng, RngCore, SeedableRng}; use rand_chacha::ChaChaRng; use secrecy::{ExposeSecret, Secret, SecretVec}; use shardtree::{error::ShardTreeError, store::memory::MemoryShardStore, ShardTree}; -use std::{ - collections::{BTreeMap, HashMap}, - convert::Infallible, - hash::Hash, - num::NonZeroU32, -}; use subtle::ConditionallySelectable; use zcash_keys::address::Address; @@ -378,13 +380,7 @@ impl pub fn test_seed(&self) -> Option<&SecretVec> { self.test_account.as_ref().map(|(seed, _)| seed) } -} -impl TestState -where - Network: consensus::Parameters, - DataStore: WalletRead, -{ /// Returns a reference to the test account, if one was configured. pub fn test_account(&self) -> Option<&TestAccount<::Account>> { self.test_account.as_ref().map(|(_, acct)| acct) @@ -1999,7 +1995,7 @@ pub trait TestCache { fn block_source(&self) -> &Self::BlockSource; /// Inserts a CompactBlock into the cache DB. - fn insert(&self, cb: &CompactBlock) -> Self::InsertResult; + fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult; } pub struct NoteCommitments { diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 1c69594467..fecf442291 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -1841,14 +1841,14 @@ mod tests { st: &mut TestState, 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(_) + Err(e) if is_account_collision(&e) ); // Remove the transparent component so that we don't have a match on the full UFVK. @@ -1869,7 +1869,7 @@ mod tests { birthday, AccountPurpose::Spending ), - Err(_) + Err(e) if is_account_collision(&e) ); } @@ -1891,7 +1891,7 @@ mod tests { birthday, AccountPurpose::Spending ), - Err(_) + Err(e) if is_account_collision(&e) ); } } @@ -1920,7 +1920,12 @@ 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] @@ -1960,7 +1965,12 @@ 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] @@ -1984,7 +1994,12 @@ 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")] diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 42c43da87d..aab0117947 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -51,7 +51,7 @@ impl TestCache for BlockCache { &self.db_cache } - fn insert(&self, cb: &CompactBlock) -> Self::InsertResult { + fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult { let cb_bytes = cb.encode_to_vec(); let res = NoteCommitments::from_compact_block(cb); self.db_cache @@ -94,7 +94,7 @@ impl TestCache for FsBlockCache { &self.db_meta } - fn insert(&self, cb: &CompactBlock) -> Self::InsertResult { + fn insert(&mut self, cb: &CompactBlock) -> Self::InsertResult { use std::io::Write; let meta = BlockMeta { diff --git a/zcash_client_sqlite/src/testing/db.rs b/zcash_client_sqlite/src/testing/db.rs index 983e875798..e980aa6ced 100644 --- a/zcash_client_sqlite/src/testing/db.rs +++ b/zcash_client_sqlite/src/testing/db.rs @@ -51,10 +51,7 @@ pub(crate) struct TestDb { } impl TestDb { - pub(crate) fn from_parts( - wallet_db: WalletDb, - data_file: NamedTempFile, - ) -> Self { + fn from_parts(wallet_db: WalletDb, data_file: NamedTempFile) -> Self { Self { wallet_db, data_file, From ee7cb69599a6ef3ec656941c1d9d101daaf0727e Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 10 Sep 2024 16:40:58 +0000 Subject: [PATCH 20/20] zcash_client_backend: Fix `tor::grpc` module feature flag We already depend on `rustls` and `webpki-roots` for `tor::http`, but `tonic` has its own feature flag that needs to be enabled for equivalent support in `tor::grpc`. We didn't need that feature flag enabled for the `proto::service::compact_tx_streamer_client` module because those constructors take a `D: TryInto`, which abstracts over TLS and leaves it up to the caller. By constrast, in `tor::grpc` we need to construct the `Endpoint` manually from a `Uri` and then configure TLS ourselves. --- zcash_client_backend/CHANGELOG.md | 4 ++++ zcash_client_backend/Cargo.toml | 23 ++++------------------- zcash_client_backend/src/tor.rs | 10 +++++----- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/zcash_client_backend/CHANGELOG.md b/zcash_client_backend/CHANGELOG.md index 3b44dce285..f79a263462 100644 --- a/zcash_client_backend/CHANGELOG.md +++ b/zcash_client_backend/CHANGELOG.md @@ -12,6 +12,10 @@ and this library adheres to Rust's notion of type instead of a type parameter. This change allows for the simplification of some type signatures. +### Fixed +- `zcash_client_backend::tor::grpc` now needs the `lightwalletd-tonic-tls-webpki-roots` + feature flag instead of `lightwalletd-tonic`, to fix compilation issues. + ## [0.13.0] - 2024-08-20 `zcash_client_backend` now supports TEX (transparent-source-only) addresses as specified diff --git a/zcash_client_backend/Cargo.toml b/zcash_client_backend/Cargo.toml index 12b5ddd664..69c28c77c1 100644 --- a/zcash_client_backend/Cargo.toml +++ b/zcash_client_backend/Cargo.toml @@ -21,17 +21,7 @@ exclude = ["*.proto"] development = ["zcash_proofs"] [package.metadata.docs.rs] -# Manually specify features while `orchard` is not in the public API. -#all-features = true -features = [ - "lightwalletd-tonic", - "transparent-inputs", - "test-dependencies", - "tor", - "unstable", - "unstable-serialization", - "unstable-spanning-tree", -] +all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] @@ -162,6 +152,9 @@ zcash_protocol = { workspace = true, features = ["local-consensus"] } ## Enables the `tonic` gRPC client bindings for connecting to a `lightwalletd` server. lightwalletd-tonic = ["dep:tonic", "hyper-util?/tokio"] +## Enables the `tls-webpki-roots` feature of `tonic`. +lightwalletd-tonic-tls-webpki-roots = ["lightwalletd-tonic", "tonic?/tls-webpki-roots"] + ## Enables the `transport` feature of `tonic` producing a fully-featured client and server implementation lightwalletd-tonic-transport = ["lightwalletd-tonic", "tonic?/transport"] @@ -229,14 +222,6 @@ 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 diff --git a/zcash_client_backend/src/tor.rs b/zcash_client_backend/src/tor.rs index ee1a636f73..8c900ab5c7 100644 --- a/zcash_client_backend/src/tor.rs +++ b/zcash_client_backend/src/tor.rs @@ -6,7 +6,7 @@ use arti_client::{config::TorClientConfigBuilder, TorClient}; use tor_rtcompat::PreferredRuntime; use tracing::debug; -#[cfg(feature = "tor-lightwalletd-tonic")] +#[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")] mod grpc; pub mod http; @@ -76,7 +76,7 @@ impl Client { pub enum Error { /// The directory passed to [`Client::create`] does not exist. MissingTorDirectory, - #[cfg(feature = "lightwalletd-tonic")] + #[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")] /// An error occurred while using gRPC-over-Tor. Grpc(self::grpc::GrpcError), /// An error occurred while using HTTP-over-Tor. @@ -91,7 +91,7 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::MissingTorDirectory => write!(f, "Tor directory is missing"), - #[cfg(feature = "lightwalletd-tonic")] + #[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")] Error::Grpc(e) => write!(f, "gRPC-over-Tor error: {}", e), Error::Http(e) => write!(f, "HTTP-over-Tor error: {}", e), Error::Io(e) => write!(f, "IO error: {}", e), @@ -104,7 +104,7 @@ impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Error::MissingTorDirectory => None, - #[cfg(feature = "lightwalletd-tonic")] + #[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")] Error::Grpc(e) => Some(e), Error::Http(e) => Some(e), Error::Io(e) => Some(e), @@ -113,7 +113,7 @@ impl std::error::Error for Error { } } -#[cfg(feature = "lightwalletd-tonic")] +#[cfg(feature = "lightwalletd-tonic-tls-webpki-roots")] impl From for Error { fn from(e: self::grpc::GrpcError) -> Self { Error::Grpc(e)