From e55df6c4938d11b2b11bd7512e35ca8b7906006b Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Thu, 5 Sep 2024 21:48:07 -0600 Subject: [PATCH] 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 | 1927 +--------------- 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, 1980 insertions(+), 1998 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b932c2fd6..c81d21f8b 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 ebaf90cc8..12b5ddd66 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 7af7ccf96..6f1ee1a93 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 75c17d8a2..a0fabf74a 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 8d092686a..9091fe4a3 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 7e8ff542c..2aaafb676 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 rusqlite::params; + 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 cb7121184..745255d20 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 daaaf41d1..6e76485cc 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 10ff63abb..6e4806938 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 fa14de41f..62750837d 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]))