diff --git a/zcash_client_sqlite/src/chain.rs b/zcash_client_sqlite/src/chain.rs index 284bc11de3..430a042094 100644 --- a/zcash_client_sqlite/src/chain.rs +++ b/zcash_client_sqlite/src/chain.rs @@ -322,32 +322,22 @@ where #[cfg(test)] #[allow(deprecated)] mod tests { - use std::num::NonZeroU32; - use sapling::zip32::ExtendedSpendingKey; - use zcash_primitives::{ - block::BlockHash, - transaction::{components::amount::NonNegativeAmount, fees::zip317::FeeRule}, - }; + use zcash_primitives::{block::BlockHash, transaction::components::amount::NonNegativeAmount}; use zcash_client_backend::{ - address::Address, - data_api::{ - chain::error::Error, wallet::input_selection::GreedyInputSelector, AccountBirthday, - WalletRead, - }, - fees::{zip317::SingleOutputChangeStrategy, DustOutputPolicy}, + data_api::{chain::error::Error, AccountBirthday, WalletRead}, scanning::ScanError, - wallet::OvkPolicy, - zip321::{Payment, TransactionRequest}, - ShieldedProtocol, }; use crate::{ - testing::{AddressType, TestBuilder}, - wallet::truncate_to_height, + testing::{self, AddressType, TestBuilder}, + wallet::{sapling::tests::SaplingPoolTester, truncate_to_height}, }; + #[cfg(feature = "orchard")] + use crate::wallet::orchard::tests::OrchardPoolTester; + #[test] fn valid_chain_states() { let mut st = TestBuilder::new() @@ -482,135 +472,46 @@ mod tests { } #[test] - fn scan_cached_blocks_allows_blocks_out_of_order() { - let mut st = TestBuilder::new() - .with_block_cache() - .with_test_account(AccountBirthday::from_sapling_activation) - .build(); - let account = st.test_account().unwrap(); - - let (_, usk, _) = st.test_account().unwrap(); - let dfvk = st.test_account_sapling().unwrap(); - - // Create a block with height SAPLING_ACTIVATION_HEIGHT - let value = NonNegativeAmount::const_from_u64(50000); - let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); - st.scan_cached_blocks(h1, 1); - assert_eq!(st.get_total_balance(account.0), value); - - // Create blocks to reach SAPLING_ACTIVATION_HEIGHT + 2 - let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); - let (h3, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); - - // Scan the later block first - st.scan_cached_blocks(h3, 1); - - // Now scan the block of height SAPLING_ACTIVATION_HEIGHT + 1 - st.scan_cached_blocks(h2, 1); - assert_eq!( - st.get_total_balance(account.0), - NonNegativeAmount::const_from_u64(150_000) - ); - - // We can spend the received notes - let req = TransactionRequest::new(vec![Payment { - recipient_address: Address::Sapling(dfvk.default_address().1), - amount: NonNegativeAmount::const_from_u64(110_000), - memo: None, - label: None, - message: None, - other_params: vec![], - }]) - .unwrap(); - let input_selector = GreedyInputSelector::new( - SingleOutputChangeStrategy::new(FeeRule::standard(), None, ShieldedProtocol::Sapling), - DustOutputPolicy::default(), - ); - assert_matches!( - st.spend( - &input_selector, - &usk, - req, - OvkPolicy::Sender, - NonZeroU32::new(1).unwrap(), - ), - Ok(_) - ); + fn scan_cached_blocks_allows_blocks_out_of_order_sapling() { + testing::pool::scan_cached_blocks_allows_blocks_out_of_order::() } #[test] - fn scan_cached_blocks_finds_received_notes() { - let mut st = TestBuilder::new() - .with_block_cache() - .with_test_account(AccountBirthday::from_sapling_activation) - .build(); - let account = st.test_account().unwrap(); - - let dfvk = st.test_account_sapling().unwrap(); - - // Wallet summary is not yet available - assert_eq!(st.get_wallet_summary(0), None); - - // Create a fake CompactBlock sending value to the address - let value = NonNegativeAmount::const_from_u64(5); - let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); - - // Scan the cache - let summary = st.scan_cached_blocks(h1, 1); - assert_eq!(summary.scanned_range().start, h1); - assert_eq!(summary.scanned_range().end, h1 + 1); - assert_eq!(summary.received_sapling_note_count(), 1); - - // Account balance should reflect the received note - assert_eq!(st.get_total_balance(account.0), value); - - // Create a second fake CompactBlock sending more value to the address - let value2 = NonNegativeAmount::const_from_u64(7); - let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value2); - - // Scan the cache again - let summary = st.scan_cached_blocks(h2, 1); - assert_eq!(summary.scanned_range().start, h2); - assert_eq!(summary.scanned_range().end, h2 + 1); - assert_eq!(summary.received_sapling_note_count(), 1); - - // Account balance should reflect both received notes - assert_eq!(st.get_total_balance(account.0), (value + value2).unwrap()); + #[cfg(feature = "orchard")] + fn scan_cached_blocks_allows_blocks_out_of_order_orchard() { + testing::pool::scan_cached_blocks_allows_blocks_out_of_order::() } #[test] - fn scan_cached_blocks_finds_change_notes() { - let mut st = TestBuilder::new() - .with_block_cache() - .with_test_account(AccountBirthday::from_sapling_activation) - .build(); - let account = st.test_account().unwrap(); - let dfvk = st.test_account_sapling().unwrap(); - - // Wallet summary is not yet available - assert_eq!(st.get_wallet_summary(0), None); - - // Create a fake CompactBlock sending value to the address - let value = NonNegativeAmount::const_from_u64(5); - let (received_height, _, nf) = - st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + fn scan_cached_blocks_finds_received_notes_sapling() { + testing::pool::scan_cached_blocks_finds_received_notes::() + } - // Scan the cache - st.scan_cached_blocks(received_height, 1); + #[test] + #[cfg(feature = "orchard")] + fn scan_cached_blocks_finds_received_notes_orchard() { + testing::pool::scan_cached_blocks_finds_received_notes::() + } - // Account balance should reflect the received note - assert_eq!(st.get_total_balance(account.0), value); + #[test] + fn scan_cached_blocks_finds_change_notes_sapling() { + testing::pool::scan_cached_blocks_finds_change_notes::() + } - // Create a second fake CompactBlock spending value from the address - let extsk2 = ExtendedSpendingKey::master(&[0]); - let to2 = extsk2.default_address().1; - let value2 = NonNegativeAmount::const_from_u64(2); - let (spent_height, _) = st.generate_next_block_spending(&dfvk, (nf, value), to2, value2); + #[test] + #[cfg(feature = "orchard")] + fn scan_cached_blocks_finds_change_notes_orchard() { + testing::pool::scan_cached_blocks_finds_change_notes::() + } - // Scan the cache again - st.scan_cached_blocks(spent_height, 1); + #[test] + fn scan_cached_blocks_detects_spends_out_of_order_sapling() { + testing::pool::scan_cached_blocks_detects_spends_out_of_order::() + } - // Account balance should equal the change - assert_eq!(st.get_total_balance(account.0), (value - value2).unwrap()); + #[test] + #[cfg(feature = "orchard")] + fn scan_cached_blocks_detects_spends_out_of_order_orchard() { + testing::pool::scan_cached_blocks_detects_spends_out_of_order::() } } diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index 309840e08b..f3f28fe39d 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -27,13 +27,16 @@ use zcash_client_backend::{ address::Address, data_api::{ self, - chain::CommitmentTreeRoot, + chain::{CommitmentTreeRoot, ScanSummary}, error::Error, wallet::input_selection::{GreedyInputSelector, GreedyInputSelectorError}, AccountBirthday, DecryptedTransaction, Ratio, WalletRead, WalletSummary, WalletWrite, }, decrypt_transaction, - fees::{fixed, standard, DustOutputPolicy}, + fees::{ + fixed::{self}, + standard, DustOutputPolicy, + }, keys::UnifiedSpendingKey, wallet::{Note, OvkPolicy, ReceivedNote}, zip321::{self, Payment, TransactionRequest}, @@ -120,6 +123,8 @@ pub(crate) trait ShieldedPoolTester { tx: &Transaction, fvk: &Self::Fvk, ) -> Result, OutputRecoveryError>; + + fn received_note_count(summary: &ScanSummary) -> usize; } pub(crate) fn send_single_step_proposed_transfer() { @@ -1397,6 +1402,143 @@ pub(crate) fn checkpoint_gaps() { ); } +pub(crate) fn scan_cached_blocks_allows_blocks_out_of_order() { + let mut st = TestBuilder::new() + .with_block_cache() + .with_test_account(AccountBirthday::from_sapling_activation) + .build(); + + let account = st.test_account().unwrap(); + let (_, usk, _) = st.test_account().unwrap(); + let dfvk = T::test_account_fvk(&st); + + let value = NonNegativeAmount::const_from_u64(50000); + let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h1, 1); + assert_eq!(st.get_total_balance(account.0), value); + + // Create blocks to reach height + 2 + let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + let (h3, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + + // Scan the later block first + st.scan_cached_blocks(h3, 1); + + // Now scan the block of height height + 1 + st.scan_cached_blocks(h2, 1); + assert_eq!( + st.get_total_balance(account.0), + NonNegativeAmount::const_from_u64(150_000) + ); + + // We can spend the received notes + let req = TransactionRequest::new(vec![Payment { + recipient_address: T::fvk_default_address(&dfvk), + amount: NonNegativeAmount::const_from_u64(110_000), + memo: None, + label: None, + message: None, + other_params: vec![], + }]) + .unwrap(); + + #[allow(deprecated)] + let input_selector = GreedyInputSelector::new( + standard::SingleOutputChangeStrategy::new( + StandardFeeRule::Zip317, + None, + T::SHIELDED_PROTOCOL, + ), + DustOutputPolicy::default(), + ); + assert_matches!( + st.spend( + &input_selector, + &usk, + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ), + Ok(_) + ); +} + +pub(crate) fn scan_cached_blocks_finds_received_notes() { + let mut st = TestBuilder::new() + .with_block_cache() + .with_test_account(AccountBirthday::from_sapling_activation) + .build(); + + let account = st.test_account().unwrap(); + let dfvk = T::test_account_fvk(&st); + + // Wallet summary is not yet available + assert_eq!(st.get_wallet_summary(0), None); + + // Create a fake CompactBlock sending value to the address + let value = NonNegativeAmount::const_from_u64(5); + let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + + // Scan the cache + let summary = st.scan_cached_blocks(h1, 1); + assert_eq!(summary.scanned_range().start, h1); + assert_eq!(summary.scanned_range().end, h1 + 1); + assert_eq!(T::received_note_count(&summary), 1); + + // Account balance should reflect the received note + assert_eq!(st.get_total_balance(account.0), value); + + // Create a second fake CompactBlock sending more value to the address + let value2 = NonNegativeAmount::const_from_u64(7); + let (h2, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value2); + + // Scan the cache again + let summary = st.scan_cached_blocks(h2, 1); + assert_eq!(summary.scanned_range().start, h2); + assert_eq!(summary.scanned_range().end, h2 + 1); + assert_eq!(T::received_note_count(&summary), 1); + + // Account balance should reflect both received notes + assert_eq!(st.get_total_balance(account.0), (value + value2).unwrap()); +} + +// TODO: This test can probably be entirely removed, as the following test duplicates it entirely. +pub(crate) fn scan_cached_blocks_finds_change_notes() { + let mut st = TestBuilder::new() + .with_block_cache() + .with_test_account(AccountBirthday::from_sapling_activation) + .build(); + + let account = st.test_account().unwrap(); + let dfvk = T::test_account_fvk(&st); + + // Wallet summary is not yet available + assert_eq!(st.get_wallet_summary(0), None); + + // Create a fake CompactBlock sending value to the address + let value = NonNegativeAmount::const_from_u64(5); + let (received_height, _, nf) = + st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + + // Scan the cache + st.scan_cached_blocks(received_height, 1); + + // Account balance should reflect the received note + assert_eq!(st.get_total_balance(account.0), value); + + // Create a second fake CompactBlock spending value from the address + let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32])); + let to2 = T::fvk_default_address(¬_our_key); + let value2 = NonNegativeAmount::const_from_u64(2); + let (spent_height, _) = st.generate_next_block_spending(&dfvk, (nf, value), to2, value2); + + // Scan the cache again + st.scan_cached_blocks(spent_height, 1); + + // Account balance should equal the change + assert_eq!(st.get_total_balance(account.0), (value - value2).unwrap()); +} + pub(crate) fn scan_cached_blocks_detects_spends_out_of_order() { let mut st = TestBuilder::new() .with_block_cache() @@ -1411,11 +1553,8 @@ pub(crate) fn scan_cached_blocks_detects_spends_out_of_order usize { + summary.received_orchard_note_count() + } } #[test] diff --git a/zcash_client_sqlite/src/wallet/sapling.rs b/zcash_client_sqlite/src/wallet/sapling.rs index b30aa17041..1410d0baad 100644 --- a/zcash_client_sqlite/src/wallet/sapling.rs +++ b/zcash_client_sqlite/src/wallet/sapling.rs @@ -510,6 +510,12 @@ pub(crate) mod tests { Ok(None) } + + fn received_note_count( + summary: &zcash_client_backend::data_api::chain::ScanSummary, + ) -> usize { + summary.received_sapling_note_count() + } } pub(crate) fn test_prover() -> impl SpendProver + OutputProver {