From bb4dd304586f694395432a9e18df820dd0e2dc28 Mon Sep 17 00:00:00 2001 From: Willem Olding Date: Tue, 10 Sep 2024 15:43:39 -0400 Subject: [PATCH] fix issue querying notes --- zcash_client_backend/src/data_api/testing.rs | 3 - .../src/data_api/testing/pool.rs | 2272 +++++++++-------- zcash_client_sqlite/src/lib.rs | 2 +- 3 files changed, 1168 insertions(+), 1109 deletions(-) diff --git a/zcash_client_backend/src/data_api/testing.rs b/zcash_client_backend/src/data_api/testing.rs index 422b13bee..6a783f7fd 100644 --- a/zcash_client_backend/src/data_api/testing.rs +++ b/zcash_client_backend/src/data_api/testing.rs @@ -100,7 +100,6 @@ pub struct TransactionSummary { memo_count: usize, expired_unmined: bool, is_shielding: bool, - raw: Vec, } impl TransactionSummary { @@ -119,7 +118,6 @@ impl TransactionSummary { memo_count: usize, expired_unmined: bool, is_shielding: bool, - raw: &[u8], ) -> Self { Self { account_id, @@ -135,7 +133,6 @@ impl TransactionSummary { memo_count, expired_unmined, is_shielding, - raw: raw.to_vec(), } } diff --git a/zcash_client_backend/src/data_api/testing/pool.rs b/zcash_client_backend/src/data_api/testing/pool.rs index bae5aa6f4..a2c60cec2 100644 --- a/zcash_client_backend/src/data_api/testing/pool.rs +++ b/zcash_client_backend/src/data_api/testing/pool.rs @@ -1,42 +1,49 @@ use assert_matches::assert_matches; -use incrementalmerkletree::Level; +use incrementalmerkletree::{frontier::Frontier, Level}; use rand::RngCore; +use secrecy::Secret; use shardtree::error::ShardTreeError; -use std::{cmp::Eq, convert::Infallible, hash::Hash, num::NonZeroU32}; +use std::{ + cmp::Eq, + convert::Infallible, + hash::Hash, + num::{NonZeroU32, NonZeroU8}, +}; use zip32::Scope; use zcash_keys::{address::Address, keys::UnifiedSpendingKey}; use zcash_primitives::{ block::BlockHash, legacy::TransparentAddress, - transaction::{components::amount::NonNegativeAmount, fees::StandardFeeRule, Transaction}, + transaction::{ + components::amount::NonNegativeAmount, + fees::{fixed::FeeRule as FixedFeeRule, StandardFeeRule}, + Transaction, + }, }; use zcash_protocol::{ - consensus::{self, BlockHeight, BranchId}, + consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters}, memo::{Memo, MemoBytes}, value::Zatoshis, - ShieldedProtocol, + PoolType, ShieldedProtocol, }; -use zip321::Payment; +use zip321::{Payment, TransactionRequest}; use crate::{ data_api::{ self, - chain::{CommitmentTreeRoot, ScanSummary}, - testing::{AddressType, TestBuilder}, - wallet::{ - decrypt_and_store_transaction, - input_selection::{GreedyInputSelector, GreedyInputSelectorError}, - }, - Account as _, DecryptedTransaction, InputSource, Ratio, WalletCommitmentTrees, WalletRead, - WalletSummary, WalletWrite, + chain::{ChainState, CommitmentTreeRoot, ScanSummary}, + testing::{input_selector, AddressType, FakeCompactOutput, InitialChainState, TestBuilder}, + wallet::{decrypt_and_store_transaction, input_selection::GreedyInputSelector}, + Account as _, AccountBirthday, DecryptedTransaction, InputSource, Ratio, + WalletCommitmentTrees, WalletRead, WalletSummary, WalletWrite, }, decrypt_transaction, - fees::{standard, DustOutputPolicy}, + fees::{fixed::SingleOutputChangeStrategy, standard, DustOutputPolicy}, wallet::{Note, NoteId, OvkPolicy, ReceivedNote}, }; -use super::{DataStoreFactory, TestCache, TestFvk, TestState}; +use super::{DataStoreFactory, Reset, TestCache, TestFvk, TestState}; /// Trait that exposes the pool-specific types and operations necessary to run the /// single-shielded-pool tests on a given pool. @@ -1149,8 +1156,7 @@ pub fn ovk_policy_prevents_recovery_from_chain( // NOTE: In prior test this was filtered by the notes value to ensure a match // unfortunately we don't have access to this here but there is only a single note // so it doesn't matter unless the test is modified + // TODO: Test failing let change_note_scope = st .wallet() .get_notes(T::SHIELDED_PROTOCOL) @@ -1297,1247 +1304,1302 @@ pub fn change_note_spends_succeed( ); } -// pub(crate) fn external_address_change_spends_detected_in_restore_from_seed< -// T: ShieldedPoolTester, -// >() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .build(); +pub fn external_address_change_spends_detected_in_restore_from_seed( + dsf: DSF, + cache: impl TestCache, +) where + DSF: DataStoreFactory, + ::DataStore: Reset, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .build(); -// // Add two accounts to the wallet. -// let seed = Secret::new([0u8; 32].to_vec()); -// let birthday = AccountBirthday::from_sapling_activation(st.network(), BlockHash([0; 32])); -// let (account_id, usk) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); -// let dfvk = T::sk_to_fvk(T::usk_to_sk(&usk)); + // Add two accounts to the wallet. + let seed = Secret::new([0u8; 32].to_vec()); + let birthday = AccountBirthday::from_sapling_activation(st.network(), BlockHash([0; 32])); + let (account_id, usk) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); + let dfvk = T::sk_to_fvk(T::usk_to_sk(&usk)); -// let (account2, usk2) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); -// let dfvk2 = T::sk_to_fvk(T::usk_to_sk(&usk2)); + let (account2, usk2) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); + let dfvk2 = T::sk_to_fvk(T::usk_to_sk(&usk2)); -// // Add funds to the wallet in a single note -// let value = NonNegativeAmount::from_u64(100000).unwrap(); -// let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); -// st.scan_cached_blocks(h, 1); + // Add funds to the wallet in a single note + let value = NonNegativeAmount::from_u64(100000).unwrap(); + let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.scan_cached_blocks(h, 1); -// // Spendable balance matches total balance -// assert_eq!(st.get_total_balance(account_id), value); -// assert_eq!(st.get_spendable_balance(account_id, 1), value); -// assert_eq!(st.get_total_balance(account2), NonNegativeAmount::ZERO); - -// let amount_sent = NonNegativeAmount::from_u64(20000).unwrap(); -// let amount_legacy_change = NonNegativeAmount::from_u64(30000).unwrap(); -// let addr = T::fvk_default_address(&dfvk); -// let addr2 = T::fvk_default_address(&dfvk2); -// let req = TransactionRequest::new(vec![ -// // payment to an external recipient -// Payment::without_memo(addr2.to_zcash_address(st.network()), amount_sent), -// // payment back to the originating wallet, simulating legacy change -// Payment::without_memo(addr.to_zcash_address(st.network()), amount_legacy_change), -// ]) -// .unwrap(); - -// #[allow(deprecated)] -// let fee_rule = FixedFeeRule::standard(); -// let input_selector = GreedyInputSelector::new( -// fixed::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL), -// DustOutputPolicy::default(), -// ); + // Spendable balance matches total balance + assert_eq!(st.get_total_balance(account_id), value); + assert_eq!(st.get_spendable_balance(account_id, 1), value); + assert_eq!(st.get_total_balance(account2), NonNegativeAmount::ZERO); + + let amount_sent = NonNegativeAmount::from_u64(20000).unwrap(); + let amount_legacy_change = NonNegativeAmount::from_u64(30000).unwrap(); + let addr = T::fvk_default_address(&dfvk); + let addr2 = T::fvk_default_address(&dfvk2); + let req = TransactionRequest::new(vec![ + // payment to an external recipient + Payment::without_memo(addr2.to_zcash_address(st.network()), amount_sent), + // payment back to the originating wallet, simulating legacy change + Payment::without_memo(addr.to_zcash_address(st.network()), amount_legacy_change), + ]) + .unwrap(); -// let txid = st -// .spend( -// &input_selector, -// &usk, -// req, -// OvkPolicy::Sender, -// NonZeroU32::new(1).unwrap(), -// ) -// .unwrap()[0]; + #[allow(deprecated)] + let fee_rule = FixedFeeRule::standard(); + let input_selector = GreedyInputSelector::new( + SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); -// let amount_left = (value - (amount_sent + fee_rule.fixed_fee()).unwrap()).unwrap(); -// let pending_change = (amount_left - amount_legacy_change).unwrap(); + let txid = st + .spend( + &input_selector, + &usk, + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ) + .unwrap()[0]; -// // The "legacy change" is not counted by get_pending_change(). -// assert_eq!(st.get_pending_change(account_id, 1), pending_change); -// // We spent the only note so we only have pending change. -// assert_eq!(st.get_total_balance(account_id), pending_change); + let amount_left = (value - (amount_sent + fee_rule.fixed_fee()).unwrap()).unwrap(); + let pending_change = (amount_left - amount_legacy_change).unwrap(); -// let (h, _) = st.generate_next_block_including(txid); -// st.scan_cached_blocks(h, 1); + // The "legacy change" is not counted by get_pending_change(). + assert_eq!(st.get_pending_change(account_id, 1), pending_change); + // We spent the only note so we only have pending change. + assert_eq!(st.get_total_balance(account_id), pending_change); -// assert_eq!(st.get_total_balance(account2), amount_sent,); -// assert_eq!(st.get_total_balance(account_id), amount_left); + let (h, _) = st.generate_next_block_including(txid); + st.scan_cached_blocks(h, 1); -// st.reset(); + assert_eq!(st.get_total_balance(account2), amount_sent,); + assert_eq!(st.get_total_balance(account_id), amount_left); -// // Account creation and DFVK derivation should be deterministic. -// let (_, restored_usk) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); -// assert!(T::fvks_equal( -// &T::sk_to_fvk(T::usk_to_sk(&restored_usk)), -// &dfvk, -// )); + st.reset(); -// let (_, restored_usk2) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); -// assert!(T::fvks_equal( -// &T::sk_to_fvk(T::usk_to_sk(&restored_usk2)), -// &dfvk2, -// )); + // Account creation and DFVK derivation should be deterministic. + let (_, restored_usk) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); + assert!(T::fvks_equal( + &T::sk_to_fvk(T::usk_to_sk(&restored_usk)), + &dfvk, + )); -// st.scan_cached_blocks(st.sapling_activation_height(), 2); + let (_, restored_usk2) = st.wallet_mut().create_account(&seed, &birthday).unwrap(); + assert!(T::fvks_equal( + &T::sk_to_fvk(T::usk_to_sk(&restored_usk2)), + &dfvk2, + )); -// assert_eq!(st.get_total_balance(account2), amount_sent,); -// assert_eq!(st.get_total_balance(account_id), amount_left); -// } + st.scan_cached_blocks(st.sapling_activation_height(), 2); -// #[allow(dead_code)] -// pub(crate) fn zip317_spend() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) -// .build(); + assert_eq!(st.get_total_balance(account2), amount_sent,); + assert_eq!(st.get_total_balance(account_id), amount_left); +} -// let account = st.test_account().cloned().unwrap(); -// let account_id = account.id(); -// let dfvk = T::test_account_fvk(&st); +#[allow(dead_code)] +pub fn zip317_spend(dsf: impl DataStoreFactory, cache: impl TestCache) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); -// // Add funds to the wallet -// let (h1, _, _) = st.generate_next_block( -// &dfvk, -// AddressType::Internal, -// NonNegativeAmount::const_from_u64(50000), -// ); + let account = st.test_account().cloned().unwrap(); + let account_id = account.id(); + let dfvk = T::test_account_fvk(&st); -// // Add 10 dust notes to the wallet -// for _ in 1..=10 { -// st.generate_next_block( -// &dfvk, -// AddressType::DefaultExternal, -// NonNegativeAmount::const_from_u64(1000), -// ); -// } + // Add funds to the wallet + let (h1, _, _) = st.generate_next_block( + &dfvk, + AddressType::Internal, + NonNegativeAmount::const_from_u64(50000), + ); -// st.scan_cached_blocks(h1, 11); + // Add 10 dust notes to the wallet + for _ in 1..=10 { + st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::const_from_u64(1000), + ); + } -// // Spendable balance matches total balance -// let total = NonNegativeAmount::const_from_u64(60000); -// assert_eq!(st.get_total_balance(account_id), total); -// assert_eq!(st.get_spendable_balance(account_id, 1), total); + st.scan_cached_blocks(h1, 11); -// let input_selector = input_selector(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL); + // Spendable balance matches total balance + let total = NonNegativeAmount::const_from_u64(60000); + assert_eq!(st.get_total_balance(account_id), total); + assert_eq!(st.get_spendable_balance(account_id, 1), total); -// // This first request will fail due to insufficient non-dust funds -// let req = TransactionRequest::new(vec![Payment::without_memo( -// T::fvk_default_address(&dfvk).to_zcash_address(st.network()), -// NonNegativeAmount::const_from_u64(50000), -// )]) -// .unwrap(); + let input_selector = input_selector(StandardFeeRule::Zip317, None, T::SHIELDED_PROTOCOL); -// assert_matches!( -// st.spend( -// &input_selector, -// account.usk(), -// req, -// OvkPolicy::Sender, -// NonZeroU32::new(1).unwrap(), -// ), -// Err(Error::InsufficientFunds { available, required }) -// if available == NonNegativeAmount::const_from_u64(51000) -// && required == NonNegativeAmount::const_from_u64(60000) -// ); + // This first request will fail due to insufficient non-dust funds + let req = TransactionRequest::new(vec![Payment::without_memo( + T::fvk_default_address(&dfvk).to_zcash_address(st.network()), + NonNegativeAmount::const_from_u64(50000), + )]) + .unwrap(); -// // This request will succeed, spending a single dust input to pay the 10000 -// // ZAT fee in addition to the 41000 ZAT output to the recipient -// let req = TransactionRequest::new(vec![Payment::without_memo( -// T::fvk_default_address(&dfvk).to_zcash_address(st.network()), -// NonNegativeAmount::const_from_u64(41000), -// )]) -// .unwrap(); - -// let txid = st -// .spend( -// &input_selector, -// account.usk(), -// req, -// OvkPolicy::Sender, -// NonZeroU32::new(1).unwrap(), -// ) -// .unwrap()[0]; + // TODO: Figure out how to handle errors + // assert_matches!( + // st.spend( + // &input_selector, + // account.usk(), + // req, + // OvkPolicy::Sender, + // NonZeroU32::new(1).unwrap(), + // ), + // Err(Error::InsufficientFunds { available, required }) + // if available == NonNegativeAmount::const_from_u64(51000) + // && required == NonNegativeAmount::const_from_u64(60000) + // ); -// let (h, _) = st.generate_next_block_including(txid); -// st.scan_cached_blocks(h, 1); + // This request will succeed, spending a single dust input to pay the 10000 + // ZAT fee in addition to the 41000 ZAT output to the recipient + let req = TransactionRequest::new(vec![Payment::without_memo( + T::fvk_default_address(&dfvk).to_zcash_address(st.network()), + NonNegativeAmount::const_from_u64(41000), + )]) + .unwrap(); -// // TODO: send to an account so that we can check its balance. -// // We sent back to the same account so the amount_sent should be included -// // in the total balance. -// assert_eq!( -// st.get_total_balance(account_id), -// (total - NonNegativeAmount::const_from_u64(10000)).unwrap() -// ); -// } + let txid = st + .spend( + &input_selector, + account.usk(), + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ) + .unwrap()[0]; -// #[cfg(feature = "transparent-inputs")] -// pub(crate) fn shield_transparent() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) -// .build(); + let (h, _) = st.generate_next_block_including(txid); + st.scan_cached_blocks(h, 1); -// let account = st.test_account().cloned().unwrap(); -// let dfvk = T::test_account_fvk(&st); + // TODO: send to an account so that we can check its balance. + // We sent back to the same account so the amount_sent should be included + // in the total balance. + assert_eq!( + st.get_total_balance(account_id), + (total - NonNegativeAmount::const_from_u64(10000)).unwrap() + ); +} -// let uaddr = st -// .wallet() -// .get_current_address(account.id()) -// .unwrap() -// .unwrap(); -// let taddr = uaddr.transparent().unwrap(); +#[cfg(feature = "transparent-inputs")] +pub fn shield_transparent(dsf: DSF, cache: impl TestCache) +where + DSF: DataStoreFactory, + <::DataStore as WalletWrite>::UtxoRef: std::fmt::Debug, +{ + use zcash_primitives::transaction::components::{OutPoint, TxOut}; -// // Ensure that the wallet has at least one block -// let (h, _, _) = st.generate_next_block( -// &dfvk, -// AddressType::Internal, -// NonNegativeAmount::const_from_u64(50000), -// ); -// st.scan_cached_blocks(h, 1); + use crate::wallet::WalletTransparentOutput; -// let utxo = WalletTransparentOutput::from_parts( -// OutPoint::fake(), -// TxOut { -// value: NonNegativeAmount::const_from_u64(100000), -// script_pubkey: taddr.script(), -// }, -// Some(h), -// ) -// .unwrap(); + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); -// let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo); -// assert_matches!(res0, Ok(_)); + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); -// let fee_rule = StandardFeeRule::Zip317; + let uaddr = st + .wallet() + .get_current_address(account.id()) + .unwrap() + .unwrap(); + let taddr = uaddr.transparent().unwrap(); -// let input_selector = GreedyInputSelector::new( -// standard::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL), -// DustOutputPolicy::default(), -// ); + // Ensure that the wallet has at least one block + let (h, _, _) = st.generate_next_block( + &dfvk, + AddressType::Internal, + NonNegativeAmount::const_from_u64(50000), + ); + st.scan_cached_blocks(h, 1); -// let txids = st -// .shield_transparent_funds( -// &input_selector, -// NonNegativeAmount::from_u64(10000).unwrap(), -// account.usk(), -// &[*taddr], -// 1, -// ) -// .unwrap(); -// assert_eq!(txids.len(), 1); - -// let tx = st.get_tx_from_history(*txids.first()).unwrap().unwrap(); -// assert_eq!(tx.spent_note_count(), 1); -// assert!(tx.has_change()); -// assert_eq!(tx.received_note_count(), 0); -// assert_eq!(tx.sent_note_count(), 0); -// assert!(tx.is_shielding()); -// } + let utxo = WalletTransparentOutput::from_parts( + OutPoint::fake(), + TxOut { + value: NonNegativeAmount::const_from_u64(100000), + script_pubkey: taddr.script(), + }, + Some(h), + ) + .unwrap(); -// // FIXME: This requires fixes to the test framework. -// #[allow(dead_code)] -// pub(crate) fn birthday_in_anchor_shard() { -// // Set up the following situation: -// // -// // |<------ 500 ------->|<--- 10 --->|<--- 10 --->| -// // last_shard_start wallet_birthday received_tx anchor_height -// // -// // We set the Sapling and Orchard frontiers at the birthday block initial state to 1234 -// // notes beyond the end of the first shard. -// let frontier_tree_size: u32 = (0x1 << 16) + 1234; -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_initial_chain_state(|rng, network| { -// let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + 1000; - -// // Construct a fake chain state for the end of the block with the given -// // birthday_offset from the Nu5 birthday. -// let (prior_sapling_roots, sapling_initial_tree) = -// Frontier::random_with_prior_subtree_roots( -// rng, -// frontier_tree_size.into(), -// NonZeroU8::new(16).unwrap(), -// ); -// // There will only be one prior root -// let prior_sapling_roots = prior_sapling_roots -// .into_iter() -// .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root)) -// .collect::>(); + let res0 = st.wallet_mut().put_received_transparent_utxo(&utxo); + assert_matches!(res0, Ok(_)); -// #[cfg(feature = "orchard")] -// let (prior_orchard_roots, orchard_initial_tree) = -// Frontier::random_with_prior_subtree_roots( -// rng, -// frontier_tree_size.into(), -// NonZeroU8::new(16).unwrap(), -// ); -// // There will only be one prior root -// #[cfg(feature = "orchard")] -// let prior_orchard_roots = prior_orchard_roots -// .into_iter() -// .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root)) -// .collect::>(); - -// InitialChainState { -// chain_state: ChainState::new( -// birthday_height - 1, -// BlockHash([5; 32]), -// sapling_initial_tree, -// #[cfg(feature = "orchard")] -// orchard_initial_tree, -// ), -// prior_sapling_roots, -// #[cfg(feature = "orchard")] -// prior_orchard_roots, -// } -// }) -// .with_account_having_current_birthday() -// .build(); + let fee_rule = StandardFeeRule::Zip317; -// // Generate 9 blocks that have no value for us, starting at the birthday height. -// let not_our_value = NonNegativeAmount::const_from_u64(10000); -// let not_our_key = T::random_fvk(st.rng_mut()); -// let (initial_height, _, _) = -// st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); -// for _ in 1..9 { -// st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); -// } + let input_selector = GreedyInputSelector::new( + standard::SingleOutputChangeStrategy::new(fee_rule, None, T::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); -// // Now, generate a block that belongs to our wallet -// let (received_tx_height, _, _) = st.generate_next_block( -// &T::test_account_fvk(&st), -// AddressType::DefaultExternal, -// NonNegativeAmount::const_from_u64(500000), -// ); + let txids = st + .shield_transparent_funds( + &input_selector, + NonNegativeAmount::from_u64(10000).unwrap(), + account.usk(), + &[*taddr], + 1, + ) + .unwrap(); + assert_eq!(txids.len(), 1); + + let tx = st.get_tx_from_history(*txids.first()).unwrap().unwrap(); + assert_eq!(tx.spent_note_count(), 1); + assert!(tx.has_change()); + assert_eq!(tx.received_note_count(), 0); + assert_eq!(tx.sent_note_count(), 0); + assert!(tx.is_shielding()); +} -// // Generate some more blocks to get above our anchor height -// for _ in 0..15 { -// st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); -// } +// FIXME: This requires fixes to the test framework. +#[allow(dead_code)] +pub fn birthday_in_anchor_shard( + dsf: impl DataStoreFactory, + cache: impl TestCache, +) { + // Set up the following situation: + // + // |<------ 500 ------->|<--- 10 --->|<--- 10 --->| + // last_shard_start wallet_birthday received_tx anchor_height + // + // We set the Sapling and Orchard frontiers at the birthday block initial state to 1234 + // notes beyond the end of the first shard. + let frontier_tree_size: u32 = (0x1 << 16) + 1234; + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_initial_chain_state(|rng, network| { + let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + 1000; + + // Construct a fake chain state for the end of the block with the given + // birthday_offset from the Nu5 birthday. + let (prior_sapling_roots, sapling_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + frontier_tree_size.into(), + NonZeroU8::new(16).unwrap(), + ); + // There will only be one prior root + let prior_sapling_roots = prior_sapling_roots + .into_iter() + .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root)) + .collect::>(); + + #[cfg(feature = "orchard")] + let (prior_orchard_roots, orchard_initial_tree) = + Frontier::random_with_prior_subtree_roots( + rng, + frontier_tree_size.into(), + NonZeroU8::new(16).unwrap(), + ); + // There will only be one prior root + #[cfg(feature = "orchard")] + let prior_orchard_roots = prior_orchard_roots + .into_iter() + .map(|root| CommitmentTreeRoot::from_parts(birthday_height - 500, root)) + .collect::>(); + + InitialChainState { + chain_state: ChainState::new( + birthday_height - 1, + BlockHash([5; 32]), + sapling_initial_tree, + #[cfg(feature = "orchard")] + orchard_initial_tree, + ), + prior_sapling_roots, + #[cfg(feature = "orchard")] + prior_orchard_roots, + } + }) + .with_account_having_current_birthday() + .build(); -// // Scan a block range that includes our received note, but skips some blocks we need to -// // make it spendable. -// st.scan_cached_blocks(initial_height + 5, 20); + // Generate 9 blocks that have no value for us, starting at the birthday height. + let not_our_value = NonNegativeAmount::const_from_u64(10000); + let not_our_key = T::random_fvk(st.rng_mut()); + let (initial_height, _, _) = + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + for _ in 1..9 { + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + } -// // Verify that the received note is not considered spendable -// let account = st.test_account().unwrap(); -// let account_id = account.id(); -// let spendable = T::select_spendable_notes( -// &st, -// account_id, -// NonNegativeAmount::const_from_u64(300000), -// received_tx_height + 10, -// &[], -// ) -// .unwrap(); - -// assert_eq!(spendable.len(), 0); - -// // Scan the blocks we skipped -// st.scan_cached_blocks(initial_height, 5); - -// // Verify that the received note is now considered spendable -// let spendable = T::select_spendable_notes( -// &st, -// account_id, -// NonNegativeAmount::const_from_u64(300000), -// received_tx_height + 10, -// &[], -// ) -// .unwrap(); - -// assert_eq!(spendable.len(), 1); -// } + // Now, generate a block that belongs to our wallet + let (received_tx_height, _, _) = st.generate_next_block( + &T::test_account_fvk(&st), + AddressType::DefaultExternal, + NonNegativeAmount::const_from_u64(500000), + ); -// pub(crate) fn checkpoint_gaps() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) -// .build(); + // Generate some more blocks to get above our anchor height + for _ in 0..15 { + st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); + } -// let account = st.test_account().cloned().unwrap(); -// let dfvk = T::test_account_fvk(&st); + // Scan a block range that includes our received note, but skips some blocks we need to + // make it spendable. + st.scan_cached_blocks(initial_height + 5, 20); -// // Generate a block with funds belonging to our wallet. -// st.generate_next_block( -// &dfvk, -// AddressType::DefaultExternal, -// NonNegativeAmount::const_from_u64(500000), -// ); -// st.scan_cached_blocks(account.birthday().height(), 1); - -// // Create a gap of 10 blocks having no shielded outputs, then add a block that doesn't -// // belong to us so that we can get a checkpoint in the tree. -// let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32])); -// let not_our_value = NonNegativeAmount::const_from_u64(10000); -// st.generate_block_at( -// account.birthday().height() + 10, -// BlockHash([0; 32]), -// &[FakeCompactOutput::new( -// ¬_our_key, -// AddressType::DefaultExternal, -// not_our_value, -// )], -// st.latest_cached_block().unwrap().sapling_end_size(), -// st.latest_cached_block().unwrap().orchard_end_size(), -// false, -// ); + // Verify that the received note is not considered spendable + let account = st.test_account().unwrap(); + let account_id = account.id(); + let spendable = T::select_spendable_notes( + &st, + account_id, + NonNegativeAmount::const_from_u64(300000), + received_tx_height + 10, + &[], + ) + .unwrap(); -// // Scan the block -// st.scan_cached_blocks(account.birthday().height() + 10, 1); + assert_eq!(spendable.len(), 0); -// // Fake that everything has been scanned -// st.wallet() -// .conn() -// .execute_batch("UPDATE scan_queue SET priority = 10") -// .unwrap(); + // Scan the blocks we skipped + st.scan_cached_blocks(initial_height, 5); -// // Verify that our note is considered spendable -// let spendable = T::select_spendable_notes( -// &st, -// account.id(), -// NonNegativeAmount::const_from_u64(300000), -// account.birthday().height() + 5, -// &[], -// ) -// .unwrap(); -// assert_eq!(spendable.len(), 1); - -// // Attempt to spend the note with 5 confirmations -// let to = T::fvk_default_address(¬_our_key); -// assert_matches!( -// st.create_spend_to_address( -// account.usk(), -// &to, -// NonNegativeAmount::const_from_u64(10000), -// None, -// OvkPolicy::Sender, -// NonZeroU32::new(5).unwrap(), -// None, -// T::SHIELDED_PROTOCOL, -// ), -// Ok(_) -// ); -// } + // Verify that the received note is now considered spendable + let spendable = T::select_spendable_notes( + &st, + account_id, + NonNegativeAmount::const_from_u64(300000), + received_tx_height + 10, + &[], + ) + .unwrap(); -// #[cfg(feature = "orchard")] -// pub(crate) fn pool_crossing_required() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard -// // activation after Sapling -// .build(); + assert_eq!(spendable.len(), 1); +} -// let account = st.test_account().cloned().unwrap(); +pub fn checkpoint_gaps(dsf: impl DataStoreFactory, cache: impl TestCache) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); -// let p0_fvk = P0::test_account_fvk(&st); + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); -// let p1_fvk = P1::test_account_fvk(&st); -// let p1_to = P1::fvk_default_address(&p1_fvk); + // Generate a block with funds belonging to our wallet. + st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::const_from_u64(500000), + ); + st.scan_cached_blocks(account.birthday().height(), 1); + + // Create a gap of 10 blocks having no shielded outputs, then add a block that doesn't + // belong to us so that we can get a checkpoint in the tree. + let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32])); + let not_our_value = NonNegativeAmount::const_from_u64(10000); + st.generate_block_at( + account.birthday().height() + 10, + BlockHash([0; 32]), + &[FakeCompactOutput::new( + ¬_our_key, + AddressType::DefaultExternal, + not_our_value, + )], + st.latest_cached_block().unwrap().sapling_end_size(), + st.latest_cached_block().unwrap().orchard_end_size(), + false, + ); -// let note_value = NonNegativeAmount::const_from_u64(350000); -// st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); -// st.scan_cached_blocks(account.birthday().height(), 2); + // Scan the block + st.scan_cached_blocks(account.birthday().height() + 10, 1); + + // Fake that everything has been scanned + // TODO: Add methods for updating scan queue + // st.wallet() + // .conn() + // .execute_batch("UPDATE scan_queue SET priority = 10") + // .unwrap(); + + // Verify that our note is considered spendable + let spendable = T::select_spendable_notes( + &st, + account.id(), + NonNegativeAmount::const_from_u64(300000), + account.birthday().height() + 5, + &[], + ) + .unwrap(); + assert_eq!(spendable.len(), 1); -// let initial_balance = note_value; -// assert_eq!(st.get_total_balance(account.id()), initial_balance); -// assert_eq!(st.get_spendable_balance(account.id(), 1), initial_balance); + // Attempt to spend the note with 5 confirmations + let to = T::fvk_default_address(¬_our_key); + assert_matches!( + st.create_spend_to_address( + account.usk(), + &to, + NonNegativeAmount::const_from_u64(10000), + None, + OvkPolicy::Sender, + NonZeroU32::new(5).unwrap(), + None, + T::SHIELDED_PROTOCOL, + ), + Ok(_) + ); +} -// let transfer_amount = NonNegativeAmount::const_from_u64(200000); -// let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo( -// p1_to.to_zcash_address(st.network()), -// transfer_amount, -// )]) -// .unwrap(); +#[cfg(feature = "orchard")] +pub fn pool_crossing_required( + dsf: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard + // activation after Sapling + .build(); -// let fee_rule = StandardFeeRule::Zip317; -// let input_selector = GreedyInputSelector::new( -// standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL), -// DustOutputPolicy::default(), -// ); -// let proposal0 = st -// .propose_transfer( -// account.id(), -// &input_selector, -// p0_to_p1, -// NonZeroU32::new(1).unwrap(), -// ) -// .unwrap(); + let account = st.test_account().cloned().unwrap(); -// let _min_target_height = proposal0.min_target_height(); -// assert_eq!(proposal0.steps().len(), 1); -// let step0 = &proposal0.steps().head; - -// // We expect 4 logical actions, two per pool (due to padding). -// let expected_fee = NonNegativeAmount::const_from_u64(20000); -// assert_eq!(step0.balance().fee_required(), expected_fee); - -// let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); -// let proposed_change = step0.balance().proposed_change(); -// assert_eq!(proposed_change.len(), 1); -// let change_output = proposed_change.get(0).unwrap(); -// // Since this is a cross-pool transfer, change will be sent to the preferred pool. -// assert_eq!( -// change_output.output_pool(), -// PoolType::Shielded(std::cmp::max( -// ShieldedProtocol::Sapling, -// ShieldedProtocol::Orchard -// )) -// ); -// assert_eq!(change_output.value(), expected_change); + let p0_fvk = P0::test_account_fvk(&st); -// let create_proposed_result = st.create_proposed_transactions::( -// account.usk(), -// OvkPolicy::Sender, -// &proposal0, -// ); -// assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); + let p1_fvk = P1::test_account_fvk(&st); + let p1_to = P1::fvk_default_address(&p1_fvk); -// let (h, _) = st.generate_next_block_including(create_proposed_result.unwrap()[0]); -// st.scan_cached_blocks(h, 1); + let note_value = NonNegativeAmount::const_from_u64(350000); + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.scan_cached_blocks(account.birthday().height(), 2); -// assert_eq!( -// st.get_total_balance(account.id()), -// (initial_balance - expected_fee).unwrap() -// ); -// assert_eq!( -// st.get_spendable_balance(account.id(), 1), -// (initial_balance - expected_fee).unwrap() -// ); -// } + let initial_balance = note_value; + assert_eq!(st.get_total_balance(account.id()), initial_balance); + assert_eq!(st.get_spendable_balance(account.id(), 1), initial_balance); -// #[cfg(feature = "orchard")] -// pub(crate) fn fully_funded_fully_private() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard -// // activation after Sapling -// .build(); + let transfer_amount = NonNegativeAmount::const_from_u64(200000); + let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo( + p1_to.to_zcash_address(st.network()), + transfer_amount, + )]) + .unwrap(); -// let account = st.test_account().cloned().unwrap(); + let fee_rule = StandardFeeRule::Zip317; + let input_selector = GreedyInputSelector::new( + standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); + let proposal0 = st + .propose_transfer( + account.id(), + &input_selector, + p0_to_p1, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); -// let p0_fvk = P0::test_account_fvk(&st); - -// let p1_fvk = P1::test_account_fvk(&st); -// let p1_to = P1::fvk_default_address(&p1_fvk); - -// let note_value = NonNegativeAmount::const_from_u64(350000); -// st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); -// st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); -// st.scan_cached_blocks(account.birthday().height(), 2); - -// let initial_balance = (note_value * 2).unwrap(); -// assert_eq!(st.get_total_balance(account.id()), initial_balance); -// assert_eq!(st.get_spendable_balance(account.id(), 1), initial_balance); - -// let transfer_amount = NonNegativeAmount::const_from_u64(200000); -// let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo( -// p1_to.to_zcash_address(st.network()), -// transfer_amount, -// )]) -// .unwrap(); - -// let fee_rule = StandardFeeRule::Zip317; -// let input_selector = GreedyInputSelector::new( -// // We set the default change output pool to P0, because we want to verify later that -// // change is actually sent to P1 (as the transaction is fully fundable from P1). -// standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL), -// DustOutputPolicy::default(), -// ); -// let proposal0 = st -// .propose_transfer( -// account.id(), -// &input_selector, -// p0_to_p1, -// NonZeroU32::new(1).unwrap(), -// ) -// .unwrap(); + let _min_target_height = proposal0.min_target_height(); + assert_eq!(proposal0.steps().len(), 1); + let step0 = &proposal0.steps().head; -// let _min_target_height = proposal0.min_target_height(); -// assert_eq!(proposal0.steps().len(), 1); -// let step0 = &proposal0.steps().head; - -// // We expect 2 logical actions, since either pool can pay the full balance required -// // and note selection should choose the fully-private path. -// let expected_fee = NonNegativeAmount::const_from_u64(10000); -// assert_eq!(step0.balance().fee_required(), expected_fee); - -// let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); -// let proposed_change = step0.balance().proposed_change(); -// assert_eq!(proposed_change.len(), 1); -// let change_output = proposed_change.get(0).unwrap(); -// // Since there are sufficient funds in either pool, change is kept in the same pool as -// // the source note (the target pool), and does not necessarily follow preference order. -// assert_eq!( -// change_output.output_pool(), -// PoolType::Shielded(P1::SHIELDED_PROTOCOL) -// ); -// assert_eq!(change_output.value(), expected_change); + // We expect 4 logical actions, two per pool (due to padding). + let expected_fee = NonNegativeAmount::const_from_u64(20000); + assert_eq!(step0.balance().fee_required(), expected_fee); -// let create_proposed_result = st.create_proposed_transactions::( -// account.usk(), -// OvkPolicy::Sender, -// &proposal0, -// ); -// assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); + let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); + let proposed_change = step0.balance().proposed_change(); + assert_eq!(proposed_change.len(), 1); + let change_output = proposed_change.get(0).unwrap(); + // Since this is a cross-pool transfer, change will be sent to the preferred pool. + assert_eq!( + change_output.output_pool(), + PoolType::Shielded(std::cmp::max( + ShieldedProtocol::Sapling, + ShieldedProtocol::Orchard + )) + ); + assert_eq!(change_output.value(), expected_change); -// let (h, _) = st.generate_next_block_including(create_proposed_result.unwrap()[0]); -// st.scan_cached_blocks(h, 1); + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal0, + ); + assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); -// assert_eq!( -// st.get_total_balance(account.id()), -// (initial_balance - expected_fee).unwrap() -// ); -// assert_eq!( -// st.get_spendable_balance(account.id(), 1), -// (initial_balance - expected_fee).unwrap() -// ); -// } + let (h, _) = st.generate_next_block_including(create_proposed_result.unwrap()[0]); + st.scan_cached_blocks(h, 1); -// #[cfg(all(feature = "orchard", feature = "transparent-inputs"))] -// pub(crate) fn fully_funded_send_to_t() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard -// // activation after Sapling -// .build(); + assert_eq!( + st.get_total_balance(account.id()), + (initial_balance - expected_fee).unwrap() + ); + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (initial_balance - expected_fee).unwrap() + ); +} -// let account = st.test_account().cloned().unwrap(); +#[cfg(feature = "orchard")] +pub fn fully_funded_fully_private( + dsf: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard + // activation after Sapling + .build(); -// let p0_fvk = P0::test_account_fvk(&st); -// let p1_fvk = P1::test_account_fvk(&st); -// let (p1_to, _) = account.usk().default_transparent_address(); - -// let note_value = NonNegativeAmount::const_from_u64(350000); -// st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); -// st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); -// st.scan_cached_blocks(account.birthday().height(), 2); - -// let initial_balance = (note_value * 2).unwrap(); -// assert_eq!(st.get_total_balance(account.id()), initial_balance); -// assert_eq!(st.get_spendable_balance(account.id(), 1), initial_balance); - -// let transfer_amount = NonNegativeAmount::const_from_u64(200000); -// let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo( -// Address::Transparent(p1_to).to_zcash_address(st.network()), -// transfer_amount, -// )]) -// .unwrap(); - -// let fee_rule = StandardFeeRule::Zip317; -// let input_selector = GreedyInputSelector::new( -// // We set the default change output pool to P0, because we want to verify later that -// // change is actually sent to P1 (as the transaction is fully fundable from P1). -// standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL), -// DustOutputPolicy::default(), -// ); -// let proposal0 = st -// .propose_transfer( -// account.id(), -// &input_selector, -// p0_to_p1, -// NonZeroU32::new(1).unwrap(), -// ) -// .unwrap(); + let account = st.test_account().cloned().unwrap(); -// let _min_target_height = proposal0.min_target_height(); -// assert_eq!(proposal0.steps().len(), 1); -// let step0 = &proposal0.steps().head; + let p0_fvk = P0::test_account_fvk(&st); -// // We expect 3 logical actions, one for the transparent output and two for the source pool. -// let expected_fee = NonNegativeAmount::const_from_u64(15000); -// assert_eq!(step0.balance().fee_required(), expected_fee); + let p1_fvk = P1::test_account_fvk(&st); + let p1_to = P1::fvk_default_address(&p1_fvk); -// let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); -// let proposed_change = step0.balance().proposed_change(); -// assert_eq!(proposed_change.len(), 1); -// let change_output = proposed_change.get(0).unwrap(); -// // Since there are sufficient funds in either pool, change is kept in the same pool as -// // the source note (the target pool), and does not necessarily follow preference order. -// // The source note will always be sapling, as we spend Sapling funds preferentially. -// assert_eq!(change_output.output_pool(), PoolType::SAPLING); -// assert_eq!(change_output.value(), expected_change); + let note_value = NonNegativeAmount::const_from_u64(350000); + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); + st.scan_cached_blocks(account.birthday().height(), 2); -// let create_proposed_result = st.create_proposed_transactions::( -// account.usk(), -// OvkPolicy::Sender, -// &proposal0, -// ); -// assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); + let initial_balance = (note_value * 2).unwrap(); + assert_eq!(st.get_total_balance(account.id()), initial_balance); + assert_eq!(st.get_spendable_balance(account.id(), 1), initial_balance); -// let (h, _) = st.generate_next_block_including(create_proposed_result.unwrap()[0]); -// st.scan_cached_blocks(h, 1); + let transfer_amount = NonNegativeAmount::const_from_u64(200000); + let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo( + p1_to.to_zcash_address(st.network()), + transfer_amount, + )]) + .unwrap(); -// assert_eq!( -// st.get_total_balance(account.id()), -// (initial_balance - transfer_amount - expected_fee).unwrap() -// ); -// assert_eq!( -// st.get_spendable_balance(account.id(), 1), -// (initial_balance - transfer_amount - expected_fee).unwrap() -// ); -// } + let fee_rule = StandardFeeRule::Zip317; + let input_selector = GreedyInputSelector::new( + // We set the default change output pool to P0, because we want to verify later that + // change is actually sent to P1 (as the transaction is fully fundable from P1). + standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); + let proposal0 = st + .propose_transfer( + account.id(), + &input_selector, + p0_to_p1, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); -// #[cfg(feature = "orchard")] -// pub(crate) fn multi_pool_checkpoint() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard -// // activation after Sapling -// .build(); + let _min_target_height = proposal0.min_target_height(); + assert_eq!(proposal0.steps().len(), 1); + let step0 = &proposal0.steps().head; + + // We expect 2 logical actions, since either pool can pay the full balance required + // and note selection should choose the fully-private path. + let expected_fee = NonNegativeAmount::const_from_u64(10000); + assert_eq!(step0.balance().fee_required(), expected_fee); + + let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); + let proposed_change = step0.balance().proposed_change(); + assert_eq!(proposed_change.len(), 1); + let change_output = proposed_change.get(0).unwrap(); + // Since there are sufficient funds in either pool, change is kept in the same pool as + // the source note (the target pool), and does not necessarily follow preference order. + assert_eq!( + change_output.output_pool(), + PoolType::Shielded(P1::SHIELDED_PROTOCOL) + ); + assert_eq!(change_output.value(), expected_change); -// let account = st.test_account().cloned().unwrap(); -// let acct_id = account.id(); + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal0, + ); + assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); -// let p0_fvk = P0::test_account_fvk(&st); -// let p1_fvk = P1::test_account_fvk(&st); + let (h, _) = st.generate_next_block_including(create_proposed_result.unwrap()[0]); + st.scan_cached_blocks(h, 1); -// // Add some funds to the wallet; we add two notes to allow successive spends. Also, -// // we will generate a note in the P1 pool to ensure that we have some tree state. -// let note_value = NonNegativeAmount::const_from_u64(500000); -// let (start_height, _, _) = -// st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); -// st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); -// st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); -// let scanned = st.scan_cached_blocks(start_height, 3); + assert_eq!( + st.get_total_balance(account.id()), + (initial_balance - expected_fee).unwrap() + ); + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (initial_balance - expected_fee).unwrap() + ); +} -// let next_to_scan = scanned.scanned_range().end; +#[cfg(all(feature = "orchard", feature = "transparent-inputs"))] +pub fn fully_funded_send_to_t( + dsf: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard + // activation after Sapling + .build(); -// let initial_balance = (note_value * 3).unwrap(); -// assert_eq!(st.get_total_balance(acct_id), initial_balance); -// assert_eq!(st.get_spendable_balance(acct_id, 1), initial_balance); + let account = st.test_account().cloned().unwrap(); -// // Generate several empty blocks -// for _ in 0..10 { -// st.generate_empty_block(); -// } + let p0_fvk = P0::test_account_fvk(&st); + let p1_fvk = P1::test_account_fvk(&st); + let (p1_to, _) = account.usk().default_transparent_address(); -// // Scan into the middle of the empty range -// let scanned = st.scan_cached_blocks(next_to_scan, 5); -// let next_to_scan = scanned.scanned_range().end; + let note_value = NonNegativeAmount::const_from_u64(350000); + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); + st.scan_cached_blocks(account.birthday().height(), 2); -// // The initial balance should be unchanged. -// assert_eq!(st.get_total_balance(acct_id), initial_balance); -// assert_eq!(st.get_spendable_balance(acct_id, 1), initial_balance); + let initial_balance = (note_value * 2).unwrap(); + assert_eq!(st.get_total_balance(account.id()), initial_balance); + assert_eq!(st.get_spendable_balance(account.id(), 1), initial_balance); -// // Set up the fee rule and input selector we'll use for all the transfers. -// let fee_rule = StandardFeeRule::Zip317; -// let input_selector = GreedyInputSelector::new( -// standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL), -// DustOutputPolicy::default(), -// ); + let transfer_amount = NonNegativeAmount::const_from_u64(200000); + let p0_to_p1 = TransactionRequest::new(vec![Payment::without_memo( + Address::Transparent(p1_to).to_zcash_address(st.network()), + transfer_amount, + )]) + .unwrap(); -// // First, send funds just to P0 -// let transfer_amount = NonNegativeAmount::const_from_u64(200000); -// let p0_transfer = TransactionRequest::new(vec![Payment::without_memo( -// P0::random_address(st.rng_mut()).to_zcash_address(st.network()), -// transfer_amount, -// )]) -// .unwrap(); -// let res = st -// .spend( -// &input_selector, -// account.usk(), -// p0_transfer, -// OvkPolicy::Sender, -// NonZeroU32::new(1).unwrap(), -// ) -// .unwrap(); -// st.generate_next_block_including(*res.first()); + let fee_rule = StandardFeeRule::Zip317; + let input_selector = GreedyInputSelector::new( + // We set the default change output pool to P0, because we want to verify later that + // change is actually sent to P1 (as the transaction is fully fundable from P1). + standard::SingleOutputChangeStrategy::new(fee_rule, None, P0::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); + let proposal0 = st + .propose_transfer( + account.id(), + &input_selector, + p0_to_p1, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); -// let expected_fee = NonNegativeAmount::const_from_u64(10000); -// let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); -// assert_eq!( -// st.get_total_balance(acct_id), -// ((note_value * 2).unwrap() + expected_change).unwrap() -// ); -// assert_eq!(st.get_pending_change(acct_id, 1), expected_change); + let _min_target_height = proposal0.min_target_height(); + assert_eq!(proposal0.steps().len(), 1); + let step0 = &proposal0.steps().head; -// // In the next block, send funds to both P0 and P1 -// let both_transfer = TransactionRequest::new(vec![ -// Payment::without_memo( -// P0::random_address(st.rng_mut()).to_zcash_address(st.network()), -// transfer_amount, -// ), -// Payment::without_memo( -// P1::random_address(st.rng_mut()).to_zcash_address(st.network()), -// transfer_amount, -// ), -// ]) -// .unwrap(); -// let res = st -// .spend( -// &input_selector, -// account.usk(), -// both_transfer, -// OvkPolicy::Sender, -// NonZeroU32::new(1).unwrap(), -// ) -// .unwrap(); -// st.generate_next_block_including(*res.first()); + // We expect 3 logical actions, one for the transparent output and two for the source pool. + let expected_fee = NonNegativeAmount::const_from_u64(15000); + assert_eq!(step0.balance().fee_required(), expected_fee); -// // Generate a few more empty blocks -// for _ in 0..5 { -// st.generate_empty_block(); -// } + let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); + let proposed_change = step0.balance().proposed_change(); + assert_eq!(proposed_change.len(), 1); + let change_output = proposed_change.get(0).unwrap(); + // Since there are sufficient funds in either pool, change is kept in the same pool as + // the source note (the target pool), and does not necessarily follow preference order. + // The source note will always be sapling, as we spend Sapling funds preferentially. + assert_eq!(change_output.output_pool(), PoolType::SAPLING); + assert_eq!(change_output.value(), expected_change); + + let create_proposed_result = st.create_proposed_transactions::( + account.usk(), + OvkPolicy::Sender, + &proposal0, + ); + assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1); -// // Generate another block with funds for us -// let (max_height, _, _) = -// st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + let (h, _) = st.generate_next_block_including(create_proposed_result.unwrap()[0]); + st.scan_cached_blocks(h, 1); -// // Scan everything. -// st.scan_cached_blocks( -// next_to_scan, -// usize::try_from(u32::from(max_height) - u32::from(next_to_scan) + 1).unwrap(), -// ); + assert_eq!( + st.get_total_balance(account.id()), + (initial_balance - transfer_amount - expected_fee).unwrap() + ); + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (initial_balance - transfer_amount - expected_fee).unwrap() + ); +} -// let expected_final = (initial_balance + note_value -// - (transfer_amount * 3).unwrap() -// - (expected_fee * 3).unwrap()) -// .unwrap(); -// assert_eq!(st.get_total_balance(acct_id), expected_final); - -// use incrementalmerkletree::Position; - -// use crate::wallet::testing::get_checkpoint_history; -// let expected_checkpoints_p0: Vec<(BlockHeight, ShieldedProtocol, Option)> = [ -// (99999, None), -// (100000, Some(0)), -// (100001, Some(1)), -// (100002, Some(1)), -// (100007, Some(1)), // synthetic checkpoint in empty span from scan start -// (100013, Some(3)), -// (100014, Some(5)), -// (100020, Some(6)), -// ] -// .into_iter() -// .map(|(h, pos)| { -// ( -// BlockHeight::from(h), -// P0::SHIELDED_PROTOCOL, -// pos.map(Position::from), -// ) -// }) -// .collect(); - -// let expected_checkpoints_p1: Vec<(BlockHeight, ShieldedProtocol, Option)> = [ -// (99999, None), -// (100000, None), -// (100001, None), -// (100002, Some(0)), -// (100007, Some(0)), // synthetic checkpoint in empty span from scan start -// (100013, Some(0)), -// (100014, Some(2)), -// (100020, Some(2)), -// ] -// .into_iter() -// .map(|(h, pos)| { -// ( -// BlockHeight::from(h), -// P1::SHIELDED_PROTOCOL, -// pos.map(Position::from), -// ) -// }) -// .collect(); +#[cfg(feature = "orchard")] +pub fn multi_pool_checkpoint( + dsf: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard + // activation after Sapling + .build(); -// let actual_checkpoints = get_checkpoint_history(st.wallet().conn()).unwrap(); + let account = st.test_account().cloned().unwrap(); + let acct_id = account.id(); -// assert_eq!( -// actual_checkpoints -// .iter() -// .filter(|(_, p, _)| p == &P0::SHIELDED_PROTOCOL) -// .cloned() -// .collect::>(), -// expected_checkpoints_p0 -// ); -// assert_eq!( -// actual_checkpoints -// .iter() -// .filter(|(_, p, _)| p == &P1::SHIELDED_PROTOCOL) -// .cloned() -// .collect::>(), -// expected_checkpoints_p1 -// ); -// } + let p0_fvk = P0::test_account_fvk(&st); + let p1_fvk = P1::test_account_fvk(&st); -// #[cfg(feature = "orchard")] -// pub(crate) fn multi_pool_checkpoints_with_pruning< -// P0: ShieldedPoolTester, -// P1: ShieldedPoolTester, -// >() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard -// // activation after Sapling -// .build(); + // Add some funds to the wallet; we add two notes to allow successive spends. Also, + // we will generate a note in the P1 pool to ensure that we have some tree state. + let note_value = NonNegativeAmount::const_from_u64(500000); + let (start_height, _, _) = + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); + let scanned = st.scan_cached_blocks(start_height, 3); -// let account = st.test_account().cloned().unwrap(); + let next_to_scan = scanned.scanned_range().end; -// let p0_fvk = P0::random_fvk(st.rng_mut()); -// let p1_fvk = P1::random_fvk(st.rng_mut()); - -// let note_value = NonNegativeAmount::const_from_u64(10000); -// // Generate 100 P0 blocks, then 100 P1 blocks, then another 100 P0 blocks. -// for _ in 0..10 { -// for _ in 0..10 { -// st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); -// } -// for _ in 0..10 { -// st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); -// } -// } -// st.scan_cached_blocks(account.birthday().height(), 200); -// for _ in 0..100 { -// st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); -// st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); -// } -// st.scan_cached_blocks(account.birthday().height() + 200, 200); -// } + let initial_balance = (note_value * 3).unwrap(); + assert_eq!(st.get_total_balance(acct_id), initial_balance); + assert_eq!(st.get_spendable_balance(acct_id, 1), initial_balance); -// pub(crate) fn valid_chain_states() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) -// .build(); + // Generate several empty blocks + for _ in 0..10 { + st.generate_empty_block(); + } -// let dfvk = T::test_account_fvk(&st); + // Scan into the middle of the empty range + let scanned = st.scan_cached_blocks(next_to_scan, 5); + let next_to_scan = scanned.scanned_range().end; -// // Empty chain should return None -// assert_matches!(st.wallet().chain_height(), Ok(None)); + // The initial balance should be unchanged. + assert_eq!(st.get_total_balance(acct_id), initial_balance); + assert_eq!(st.get_spendable_balance(acct_id, 1), initial_balance); -// // Create a fake CompactBlock sending value to the address -// let (h1, _, _) = st.generate_next_block( -// &dfvk, -// AddressType::DefaultExternal, -// NonNegativeAmount::const_from_u64(5), -// ); + // Set up the fee rule and input selector we'll use for all the transfers. + let fee_rule = StandardFeeRule::Zip317; + let input_selector = GreedyInputSelector::new( + standard::SingleOutputChangeStrategy::new(fee_rule, None, P1::SHIELDED_PROTOCOL), + DustOutputPolicy::default(), + ); -// // Scan the cache -// st.scan_cached_blocks(h1, 1); + // First, send funds just to P0 + let transfer_amount = NonNegativeAmount::const_from_u64(200000); + let p0_transfer = TransactionRequest::new(vec![Payment::without_memo( + P0::random_address(st.rng_mut()).to_zcash_address(st.network()), + transfer_amount, + )]) + .unwrap(); + let res = st + .spend( + &input_selector, + account.usk(), + p0_transfer, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + st.generate_next_block_including(*res.first()); -// // Create a second fake CompactBlock sending more value to the address -// let (h2, _, _) = st.generate_next_block( -// &dfvk, -// AddressType::DefaultExternal, -// NonNegativeAmount::const_from_u64(7), -// ); + let expected_fee = NonNegativeAmount::const_from_u64(10000); + let expected_change = (note_value - transfer_amount - expected_fee).unwrap(); + assert_eq!( + st.get_total_balance(acct_id), + ((note_value * 2).unwrap() + expected_change).unwrap() + ); + assert_eq!(st.get_pending_change(acct_id, 1), expected_change); -// // Scanning should detect no inconsistencies -// st.scan_cached_blocks(h2, 1); -// } + // In the next block, send funds to both P0 and P1 + let both_transfer = TransactionRequest::new(vec![ + Payment::without_memo( + P0::random_address(st.rng_mut()).to_zcash_address(st.network()), + transfer_amount, + ), + Payment::without_memo( + P1::random_address(st.rng_mut()).to_zcash_address(st.network()), + transfer_amount, + ), + ]) + .unwrap(); + let res = st + .spend( + &input_selector, + account.usk(), + both_transfer, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ) + .unwrap(); + st.generate_next_block_including(*res.first()); -// // FIXME: This requires fixes to the test framework. -// #[allow(dead_code)] -// pub(crate) fn invalid_chain_cache_disconnected() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) -// .build(); + // Generate a few more empty blocks + for _ in 0..5 { + st.generate_empty_block(); + } -// let dfvk = T::test_account_fvk(&st); + // Generate another block with funds for us + let (max_height, _, _) = + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); -// // Create some fake CompactBlocks -// let (h, _, _) = st.generate_next_block( -// &dfvk, -// AddressType::DefaultExternal, -// NonNegativeAmount::const_from_u64(5), -// ); -// let (last_contiguous_height, _, _) = st.generate_next_block( -// &dfvk, -// AddressType::DefaultExternal, -// NonNegativeAmount::const_from_u64(7), -// ); + // Scan everything. + st.scan_cached_blocks( + next_to_scan, + usize::try_from(u32::from(max_height) - u32::from(next_to_scan) + 1).unwrap(), + ); -// // Scanning the cache should find no inconsistencies -// st.scan_cached_blocks(h, 2); - -// // Create more fake CompactBlocks that don't connect to the scanned ones -// let disconnect_height = last_contiguous_height + 1; -// st.generate_block_at( -// disconnect_height, -// BlockHash([1; 32]), -// &[FakeCompactOutput::new( -// &dfvk, -// AddressType::DefaultExternal, -// NonNegativeAmount::const_from_u64(8), -// )], -// 2, -// 2, -// true, -// ); -// st.generate_next_block( -// &dfvk, -// AddressType::DefaultExternal, -// NonNegativeAmount::const_from_u64(3), -// ); + let expected_final = (initial_balance + note_value + - (transfer_amount * 3).unwrap() + - (expected_fee * 3).unwrap()) + .unwrap(); + assert_eq!(st.get_total_balance(acct_id), expected_final); + + use incrementalmerkletree::Position; + + let expected_checkpoints_p0: Vec<(BlockHeight, ShieldedProtocol, Option)> = [ + (99999, None), + (100000, Some(0)), + (100001, Some(1)), + (100002, Some(1)), + (100007, Some(1)), // synthetic checkpoint in empty span from scan start + (100013, Some(3)), + (100014, Some(5)), + (100020, Some(6)), + ] + .into_iter() + .map(|(h, pos)| { + ( + BlockHeight::from(h), + P0::SHIELDED_PROTOCOL, + pos.map(Position::from), + ) + }) + .collect(); + + let expected_checkpoints_p1: Vec<(BlockHeight, ShieldedProtocol, Option)> = [ + (99999, None), + (100000, None), + (100001, None), + (100002, Some(0)), + (100007, Some(0)), // synthetic checkpoint in empty span from scan start + (100013, Some(0)), + (100014, Some(2)), + (100020, Some(2)), + ] + .into_iter() + .map(|(h, pos)| { + ( + BlockHeight::from(h), + P1::SHIELDED_PROTOCOL, + pos.map(Position::from), + ) + }) + .collect(); + + // TODO: Add method for getting checkpoint history without db + // let actual_checkpoints = get_checkpoint_history(st.wallet().conn()).unwrap(); + + // assert_eq!( + // actual_checkpoints + // .iter() + // .filter(|(_, p, _)| p == &P0::SHIELDED_PROTOCOL) + // .cloned() + // .collect::>(), + // expected_checkpoints_p0 + // ); + // assert_eq!( + // actual_checkpoints + // .iter() + // .filter(|(_, p, _)| p == &P1::SHIELDED_PROTOCOL) + // .cloned() + // .collect::>(), + // expected_checkpoints_p1 + // ); +} -// // Data+cache chain should be invalid at the data/cache boundary -// assert_matches!( -// st.try_scan_cached_blocks( -// disconnect_height, -// 2 -// ), -// Err(chain::error::Error::Scan(ScanError::PrevHashMismatch { at_height })) -// if at_height == disconnect_height -// ); -// } +#[cfg(feature = "orchard")] +pub fn multi_pool_checkpoints_with_pruning( + dsf: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) // TODO: Allow for Orchard + // activation after Sapling + .build(); -// pub(crate) fn data_db_truncation() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) -// .build(); + let account = st.test_account().cloned().unwrap(); -// let account = st.test_account().cloned().unwrap(); -// let dfvk = T::test_account_fvk(&st); + let p0_fvk = P0::random_fvk(st.rng_mut()); + let p1_fvk = P1::random_fvk(st.rng_mut()); + + let note_value = NonNegativeAmount::const_from_u64(10000); + // Generate 100 P0 blocks, then 100 P1 blocks, then another 100 P0 blocks. + for _ in 0..10 { + for _ in 0..10 { + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + } + for _ in 0..10 { + st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); + } + } + st.scan_cached_blocks(account.birthday().height(), 200); + for _ in 0..100 { + st.generate_next_block(&p0_fvk, AddressType::DefaultExternal, note_value); + st.generate_next_block(&p1_fvk, AddressType::DefaultExternal, note_value); + } + st.scan_cached_blocks(account.birthday().height() + 200, 200); +} -// // Wallet summary is not yet available -// assert_eq!(st.get_wallet_summary(0), None); +pub fn valid_chain_states( + dsf: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); -// // Create fake CompactBlocks sending value to the address -// let value = NonNegativeAmount::const_from_u64(5); -// let value2 = NonNegativeAmount::const_from_u64(7); -// let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); -// st.generate_next_block(&dfvk, AddressType::DefaultExternal, value2); + let dfvk = T::test_account_fvk(&st); -// // Scan the cache -// st.scan_cached_blocks(h, 2); + // Empty chain should return None + assert_matches!(st.wallet().chain_height(), Ok(None)); -// // Spendable balance should reflect both received notes -// assert_eq!( -// st.get_spendable_balance(account.id(), 1), -// (value + value2).unwrap() -// ); + // Create a fake CompactBlock sending value to the address + let (h1, _, _) = st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::const_from_u64(5), + ); -// // "Rewind" to height of last scanned block (this is a no-op) -// st.wallet_mut() -// .db_mut() -// .transactionally(|wdb| truncate_to_height(wdb.conn.0, &wdb.params, h + 1)) -// .unwrap(); + // Scan the cache + st.scan_cached_blocks(h1, 1); -// // Spendable balance should be unaltered -// assert_eq!( -// st.get_spendable_balance(account.id(), 1), -// (value + value2).unwrap() -// ); + // Create a second fake CompactBlock sending more value to the address + let (h2, _, _) = st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::const_from_u64(7), + ); -// // Rewind so that one block is dropped -// st.wallet_mut() -// .db_mut() -// .transactionally(|wdb| truncate_to_height(wdb.conn.0, &wdb.params, h)) -// .unwrap(); + // Scanning should detect no inconsistencies + st.scan_cached_blocks(h2, 1); +} -// // Spendable balance should only contain the first received note; -// // the rest should be pending. -// assert_eq!(st.get_spendable_balance(account.id(), 1), value); -// assert_eq!(st.get_pending_shielded_balance(account.id(), 1), value2); +// FIXME: This requires fixes to the test framework. +#[allow(dead_code)] +pub fn invalid_chain_cache_disconnected( + dsf: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); -// // Scan the cache again -// st.scan_cached_blocks(h, 2); + let dfvk = T::test_account_fvk(&st); -// // Account balance should again reflect both received notes -// assert_eq!( -// st.get_spendable_balance(account.id(), 1), -// (value + value2).unwrap() -// ); -// } + // Create some fake CompactBlocks + let (h, _, _) = st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::const_from_u64(5), + ); + let (last_contiguous_height, _, _) = st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::const_from_u64(7), + ); -// pub(crate) fn scan_cached_blocks_allows_blocks_out_of_order() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) -// .build(); + // Scanning the cache should find no inconsistencies + st.scan_cached_blocks(h, 2); -// let account = st.test_account().cloned().unwrap(); -// let dfvk = T::test_account_fvk(&st); + // Create more fake CompactBlocks that don't connect to the scanned ones + let disconnect_height = last_contiguous_height + 1; + st.generate_block_at( + disconnect_height, + BlockHash([1; 32]), + &[FakeCompactOutput::new( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::const_from_u64(8), + )], + 2, + 2, + true, + ); + st.generate_next_block( + &dfvk, + AddressType::DefaultExternal, + NonNegativeAmount::const_from_u64(3), + ); -// 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.id()), value); + // Data+cache chain should be invalid at the data/cache boundary + // TODO: Requires error handling + // assert_matches!( + // st.try_scan_cached_blocks( + // disconnect_height, + // 2 + // ), + // Err(chain::error::Error::Scan(ScanError::PrevHashMismatch { at_height })) + // if at_height == disconnect_height + // ); +} -// // 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); +pub fn data_db_truncation(dsf: DSF, cache: impl TestCache) +where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); -// // Scan the later block first -// st.scan_cached_blocks(h3, 1); + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); -// // Now scan the block of height height + 1 -// st.scan_cached_blocks(h2, 1); -// assert_eq!( -// st.get_total_balance(account.id()), -// NonNegativeAmount::const_from_u64(150_000) -// ); + // Wallet summary is not yet available + assert_eq!(st.get_wallet_summary(0), None); -// // We can spend the received notes -// let req = TransactionRequest::new(vec![Payment::without_memo( -// T::fvk_default_address(&dfvk).to_zcash_address(st.network()), -// NonNegativeAmount::const_from_u64(110_000), -// )]) -// .unwrap(); + // Create fake CompactBlocks sending value to the address + let value = NonNegativeAmount::const_from_u64(5); + let value2 = NonNegativeAmount::const_from_u64(7); + let (h, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value); + st.generate_next_block(&dfvk, AddressType::DefaultExternal, value2); -// #[allow(deprecated)] -// let input_selector = GreedyInputSelector::new( -// standard::SingleOutputChangeStrategy::new( -// StandardFeeRule::Zip317, -// None, -// T::SHIELDED_PROTOCOL, -// ), -// DustOutputPolicy::default(), -// ); -// assert_matches!( -// st.spend( -// &input_selector, -// account.usk(), -// req, -// OvkPolicy::Sender, -// NonZeroU32::new(1).unwrap(), -// ), -// Ok(_) -// ); -// } + // Scan the cache + st.scan_cached_blocks(h, 2); -// pub(crate) fn scan_cached_blocks_finds_received_notes() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) -// .build(); + // Spendable balance should reflect both received notes + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (value + value2).unwrap() + ); -// let account = st.test_account().cloned().unwrap(); -// let dfvk = T::test_account_fvk(&st); + // "Rewind" to height of last scanned block (this is a no-op) + st.wallet_mut().truncate_to_height(h + 1).unwrap(); -// // Wallet summary is not yet available -// assert_eq!(st.get_wallet_summary(0), None); + // Spendable balance should be unaltered + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (value + value2).unwrap() + ); -// // 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); + // Rewind so that one block is dropped + st.wallet_mut().truncate_to_height(h).unwrap(); -// // 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); + // Spendable balance should only contain the first received note; + // the rest should be pending. + assert_eq!(st.get_spendable_balance(account.id(), 1), value); + assert_eq!(st.get_pending_shielded_balance(account.id(), 1), value2); -// // Account balance should reflect the received note -// assert_eq!(st.get_total_balance(account.id()), value); + // Scan the cache again + st.scan_cached_blocks(h, 2); -// // 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); + // Account balance should again reflect both received notes + assert_eq!( + st.get_spendable_balance(account.id(), 1), + (value + value2).unwrap() + ); +} -// // 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); +pub fn scan_cached_blocks_allows_blocks_out_of_order( + dsf: impl DataStoreFactory, + cache: impl TestCache, +) { + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); -// // Account balance should reflect both received notes -// assert_eq!( -// st.get_total_balance(account.id()), -// (value + value2).unwrap() -// ); -// } + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); -// // TODO: This test can probably be entirely removed, as the following test duplicates it entirely. -// pub(crate) fn scan_cached_blocks_finds_change_notes() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) -// .build(); + 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.id()), value); -// let account = st.test_account().cloned().unwrap(); -// let dfvk = T::test_account_fvk(&st); + // 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); -// // Wallet summary is not yet available -// assert_eq!(st.get_wallet_summary(0), None); + // Scan the later block first + st.scan_cached_blocks(h3, 1); -// // 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); + // Now scan the block of height height + 1 + st.scan_cached_blocks(h2, 1); + assert_eq!( + st.get_total_balance(account.id()), + NonNegativeAmount::const_from_u64(150_000) + ); -// // Scan the cache -// st.scan_cached_blocks(received_height, 1); + // We can spend the received notes + let req = TransactionRequest::new(vec![Payment::without_memo( + T::fvk_default_address(&dfvk).to_zcash_address(st.network()), + NonNegativeAmount::const_from_u64(110_000), + )]) + .unwrap(); -// // Account balance should reflect the received note -// assert_eq!(st.get_total_balance(account.id()), value); + #[allow(deprecated)] + let input_selector = GreedyInputSelector::new( + standard::SingleOutputChangeStrategy::new( + StandardFeeRule::Zip317, + None, + T::SHIELDED_PROTOCOL, + ), + DustOutputPolicy::default(), + ); + assert_matches!( + st.spend( + &input_selector, + account.usk(), + req, + OvkPolicy::Sender, + NonZeroU32::new(1).unwrap(), + ), + Ok(_) + ); +} -// // 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); +pub fn scan_cached_blocks_finds_received_notes( + dsf: DSF, + cache: impl TestCache, +) where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); -// // Scan the cache again -// st.scan_cached_blocks(spent_height, 1); + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); -// // Account balance should equal the change -// assert_eq!( -// st.get_total_balance(account.id()), -// (value - value2).unwrap() -// ); -// } + // Wallet summary is not yet available + assert_eq!(st.get_wallet_summary(0), None); -// pub(crate) fn scan_cached_blocks_detects_spends_out_of_order() { -// let mut st = TestBuilder::new() -// .with_data_store_factory(TestDbFactory) -// .with_block_cache(BlockCache::new()) -// .with_account_from_sapling_activation(BlockHash([0; 32])) -// .build(); + // 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); -// let account = st.test_account().cloned().unwrap(); -// let dfvk = T::test_account_fvk(&st); + // 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); -// // Wallet summary is not yet available -// assert_eq!(st.get_wallet_summary(0), None); + // Account balance should reflect the received note + assert_eq!(st.get_total_balance(account.id()), value); -// // 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); + // 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); -// // 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 + 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); -// // Scan the spending block first. -// st.scan_cached_blocks(spent_height, 1); + // Account balance should reflect both received notes + assert_eq!( + st.get_total_balance(account.id()), + (value + value2).unwrap() + ); +} -// // Account balance should equal the change -// assert_eq!( -// st.get_total_balance(account.id()), -// (value - value2).unwrap() -// ); +// TODO: This test can probably be entirely removed, as the following test duplicates it entirely. +pub fn scan_cached_blocks_finds_change_notes( + dsf: DSF, + cache: impl TestCache, +) where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); -// // Now scan the block in which we received the note that was spent. -// st.scan_cached_blocks(received_height, 1); + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); -// // Account balance should be the same. -// assert_eq!( -// st.get_total_balance(account.id()), -// (value - value2).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); + + // Scan the cache + st.scan_cached_blocks(received_height, 1); + + // Account balance should reflect the received note + assert_eq!(st.get_total_balance(account.id()), value); + + // Create a second fake CompactBlock spending value from the address + let not_our_key = T::sk_to_fvk(&T::sk(&[0xf5; 32])); + 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.id()), + (value - value2).unwrap() + ); +} + +pub fn scan_cached_blocks_detects_spends_out_of_order( + dsf: DSF, + cache: impl TestCache, +) where + DSF: DataStoreFactory, + ::AccountId: std::fmt::Debug, +{ + let mut st = TestBuilder::new() + .with_data_store_factory(dsf) + .with_block_cache(cache) + .with_account_from_sapling_activation(BlockHash([0; 32])) + .build(); + + let account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + + // 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); + + // 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 spending block first. + st.scan_cached_blocks(spent_height, 1); + + // Account balance should equal the change + assert_eq!( + st.get_total_balance(account.id()), + (value - value2).unwrap() + ); + + // Now scan the block in which we received the note that was spent. + st.scan_cached_blocks(received_height, 1); + + // Account balance should be the same. + assert_eq!( + st.get_total_balance(account.id()), + (value - value2).unwrap() + ); +} diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index f891a03ec..6aee661b9 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -334,7 +334,7 @@ impl, P: consensus::Parameters> InputSource for let (table_prefix, index_col, note_reconstruction_cols) = per_protocol_names(protocol); let mut stmt_sent_notes = self.conn.borrow().prepare(&format!( - "SELECT txid, output_index + "SELECT txid, {index_col} FROM {table_prefix}_received_notes rn INNER JOIN transactions ON transactions.id_tx = rn.tx AND transactions.block IS NOT NULL