diff --git a/zcash_client_sqlite/src/testing.rs b/zcash_client_sqlite/src/testing.rs index 83d0cd173..32b25d7b6 100644 --- a/zcash_client_sqlite/src/testing.rs +++ b/zcash_client_sqlite/src/testing.rs @@ -235,7 +235,7 @@ impl TestBuilder { let mut cached_blocks = BTreeMap::new(); - if let Some(initial_state) = self.initial_chain_state { + if let Some(initial_state) = &self.initial_chain_state { db_data .put_sapling_subtree_roots(0, &initial_state.prior_sapling_roots) .unwrap(); @@ -302,7 +302,9 @@ impl TestBuilder { TestState { cache: self.cache, cached_blocks, - latest_block_height: None, + latest_block_height: self + .initial_chain_state + .map(|s| s.chain_state.block_height()), _data_file: data_file, db_data, test_account, @@ -474,6 +476,7 @@ where value, prior_cached_block.sapling_end_size, prior_cached_block.orchard_end_size, + false, ); (height, res, nf) @@ -529,6 +532,7 @@ where value: NonNegativeAmount, initial_sapling_tree_size: u32, initial_orchard_tree_size: u32, + allow_broken_hash_chain: bool, ) -> (Cache::InsertResult, Fvk::Nullifier) { let mut prior_cached_block = self .latest_cached_block_below_height(height) @@ -542,7 +546,9 @@ where // we need to generate a new prior cached block that the block to be generated can // successfully chain from, with the provided tree sizes. if prior_cached_block.chain_state.block_height() == height - 1 { - assert_eq!(prev_hash, prior_cached_block.chain_state.block_hash()); + if !allow_broken_hash_chain { + assert_eq!(prev_hash, prior_cached_block.chain_state.block_hash()); + } } else { let final_sapling_tree = (prior_cached_block.sapling_end_size..initial_sapling_tree_size).fold( @@ -774,6 +780,11 @@ impl TestState { &mut self.db_data } + /// Exposes the test framework's source of randomness. + pub(crate) fn rng_mut(&mut self) -> &mut ChaChaRng { + &mut self.rng + } + /// Exposes the network in use. pub(crate) fn network(&self) -> LocalNetwork { self.db_data.params diff --git a/zcash_client_sqlite/src/testing/pool.rs b/zcash_client_sqlite/src/testing/pool.rs index f23fecf28..c0a37cc71 100644 --- a/zcash_client_sqlite/src/testing/pool.rs +++ b/zcash_client_sqlite/src/testing/pool.rs @@ -2,16 +2,19 @@ //! //! Generalised for sharing across the Sapling and Orchard implementations. -use std::{convert::Infallible, num::NonZeroU32}; +use std::{ + convert::Infallible, + num::{NonZeroU32, NonZeroU8}, +}; -use incrementalmerkletree::Level; +use incrementalmerkletree::{frontier::Frontier, Level}; use rand_core::RngCore; use rusqlite::params; use secrecy::Secret; use shardtree::error::ShardTreeError; use zcash_primitives::{ block::BlockHash, - consensus::BranchId, + consensus::{BranchId, NetworkUpgrade, Parameters}, legacy::TransparentAddress, memo::{Memo, MemoBytes}, transaction::{ @@ -28,7 +31,7 @@ use zcash_client_backend::{ address::Address, data_api::{ self, - chain::{self, CommitmentTreeRoot, ScanSummary}, + chain::{self, ChainState, CommitmentTreeRoot, ScanSummary}, error::Error, wallet::input_selection::{GreedyInputSelector, GreedyInputSelectorError}, AccountBirthday, DecryptedTransaction, Ratio, WalletRead, WalletSummary, WalletWrite, @@ -46,11 +49,8 @@ use zcash_protocol::consensus::BlockHeight; use super::TestFvk; use crate::{ error::SqliteClientError, - testing::{input_selector, AddressType, BlockCache, TestBuilder, TestState}, - wallet::{ - block_max_scanned, commitment_tree, parse_scope, - scanning::tests::test_with_nu5_birthday_offset, truncate_to_height, - }, + testing::{input_selector, AddressType, BlockCache, InitialChainState, TestBuilder, TestState}, + wallet::{block_max_scanned, commitment_tree, parse_scope, truncate_to_height}, AccountId, NoteId, ReceivedNoteId, }; @@ -1260,66 +1260,75 @@ pub(crate) fn shield_transparent() { // FIXME: This requires fixes to the test framework. #[allow(dead_code)] pub(crate) fn birthday_in_anchor_shard() { - // Use a non-zero birthday offset because Sapling and NU5 are activated at the same height. - let (mut st, dfvk, birthday, _) = test_with_nu5_birthday_offset::(76, BlockHash([0; 32])); - // Set up the following situation: // // |<------ 500 ------->|<--- 10 --->|<--- 10 --->| // last_shard_start wallet_birthday received_tx anchor_height // - // Set up some shard root history before the wallet birthday. - let prev_shard_start = birthday.height() - 500; - T::put_subtree_roots( - &mut st, - 0, - &[CommitmentTreeRoot::from_parts( - prev_shard_start, - // fake a hash, the value doesn't matter - T::empty_tree_leaf(), - )], - ) - .unwrap(); + // 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_block_cache() + .with_initial_chain_state(|rng, network| { + let birthday_height = network.activation_height(NetworkUpgrade::Nu5).unwrap() + 1000; - let received_tx_height = birthday.height() + 10; + // 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 initial_sapling_tree_size = birthday - .sapling_frontier() - .value() - .map(|f| u64::from(f.position() + 1)) - .unwrap_or(0) - .try_into() - .unwrap(); - #[cfg(feature = "orchard")] - let initial_orchard_tree_size = birthday - .orchard_frontier() - .value() - .map(|f| u64::from(f.position() + 1)) - .unwrap_or(0) - .try_into() - .unwrap(); - #[cfg(not(feature = "orchard"))] - let initial_orchard_tree_size = 0; + #[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(); // Generate 9 blocks that have no value for us, starting at the birthday height. - 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( - birthday.height(), - BlockHash([0; 32]), - ¬_our_key, - AddressType::DefaultExternal, - not_our_value, - initial_sapling_tree_size, - initial_orchard_tree_size, - ); + 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); } // Now, generate a block that belongs to our wallet - st.generate_next_block( - &dfvk, + let (received_tx_height, _, _) = st.generate_next_block( + &T::test_account_fvk(&st), AddressType::DefaultExternal, NonNegativeAmount::const_from_u64(500000), ); @@ -1331,7 +1340,7 @@ pub(crate) fn birthday_in_anchor_shard() { // Scan a block range that includes our received note, but skips some blocks we need to // make it spendable. - st.scan_cached_blocks(birthday.height() + 5, 20); + st.scan_cached_blocks(initial_height + 5, 20); // Verify that the received note is not considered spendable let account = st.test_account().unwrap(); @@ -1348,7 +1357,7 @@ pub(crate) fn birthday_in_anchor_shard() { assert_eq!(spendable.len(), 0); // Scan the blocks we skipped - st.scan_cached_blocks(birthday.height(), 5); + st.scan_cached_blocks(initial_height, 5); // Verify that the received note is now considered spendable let spendable = T::select_spendable_notes( @@ -1392,6 +1401,7 @@ pub(crate) fn checkpoint_gaps() { not_our_value, st.latest_cached_block().unwrap().sapling_end_size, st.latest_cached_block().unwrap().orchard_end_size, + false, ); // Scan the block @@ -1888,6 +1898,7 @@ pub(crate) fn invalid_chain_cache_disconnected() { NonNegativeAmount::const_from_u64(8), 2, 2, + true, ); st.generate_next_block( &dfvk, diff --git a/zcash_client_sqlite/src/wallet.rs b/zcash_client_sqlite/src/wallet.rs index 9967b1eab..9fd7848d3 100644 --- a/zcash_client_sqlite/src/wallet.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -374,19 +374,27 @@ pub(crate) fn add_account( #[cfg(not(feature = "transparent-inputs"))] let transparent_item: Option> = None; + let birthday_sapling_tree_size = Some(birthday.sapling_frontier().tree_size()); + #[cfg(feature = "orchard")] + let birthday_orchard_tree_size = Some(birthday.orchard_frontier().tree_size()); + #[cfg(not(feature = "orchard"))] + let birthday_orchard_tree_size: Option = None; + let account_id: AccountId = conn.query_row( r#" INSERT INTO accounts ( account_kind, hd_seed_fingerprint, hd_account_index, ufvk, uivk, orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache, - birthday_height, recover_until_height + birthday_height, birthday_sapling_tree_size, birthday_orchard_tree_size, + recover_until_height ) VALUES ( :account_kind, :hd_seed_fingerprint, :hd_account_index, :ufvk, :uivk, :orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache, - :birthday_height, :recover_until_height + :birthday_height, :birthday_sapling_tree_size, :birthday_orchard_tree_size, + :recover_until_height ) RETURNING id; "#, @@ -400,6 +408,8 @@ pub(crate) fn add_account( ":sapling_fvk_item_cache": sapling_item, ":p2pkh_fvk_item_cache": transparent_item, ":birthday_height": u32::from(birthday.height()), + ":birthday_sapling_tree_size": birthday_sapling_tree_size, + ":birthday_orchard_tree_size": birthday_orchard_tree_size, ":recover_until_height": birthday.recover_until().map(u32::from) ], |row| Ok(AccountId(row.get(0)?)), @@ -884,22 +894,39 @@ impl ScanProgress for SubtreeScanProgress { ) .map_err(SqliteClientError::from) } else { - let start_height = birthday_height; - // Compute the starting number of notes directly from the blocks table - let start_size = conn.query_row( - "SELECT MAX(sapling_commitment_tree_size) - FROM blocks - WHERE height <= :start_height", - named_params![":start_height": u32::from(start_height)], - |row| row.get::<_, Option>(0), - )?; + // Get the starting note commitment tree size from the wallet birthday, or failing that + // from the blocks table. + let start_size = conn + .query_row( + "SELECT birthday_sapling_tree_size + FROM accounts + WHERE birthday_height = :birthday_height", + named_params![":birthday_height": u32::from(birthday_height)], + |row| row.get::<_, Option>(0), + ) + .optional()? + .flatten() + .map(Ok) + .or_else(|| { + conn.query_row( + "SELECT MAX(sapling_commitment_tree_size - sapling_output_count) + FROM blocks + WHERE height <= :start_height", + named_params![":start_height": u32::from(birthday_height)], + |row| row.get::<_, Option>(0), + ) + .optional() + .map(|opt| opt.flatten()) + .transpose() + }) + .transpose()?; // Compute the total blocks scanned so far above the starting height let scanned_count = conn.query_row( "SELECT SUM(sapling_output_count) FROM blocks WHERE height > :start_height", - named_params![":start_height": u32::from(start_height)], + named_params![":start_height": u32::from(birthday_height)], |row| row.get::<_, Option>(0), )?; @@ -915,22 +942,22 @@ impl ScanProgress for SubtreeScanProgress { FROM sapling_tree_shards WHERE subtree_end_height > :start_height OR subtree_end_height IS NULL", - named_params![":start_height": u32::from(start_height)], + named_params![":start_height": u32::from(birthday_height)], |row| { let min_tree_size = row .get::<_, Option>(0)? - .map(|min| min << SAPLING_SHARD_HEIGHT); - let max_idx = row.get::<_, Option>(1)?; - Ok(start_size - .or(min_tree_size) - .zip(max_idx) - .map(|(min_tree_size, max)| { - let max_tree_size = (max + 1) << SAPLING_SHARD_HEIGHT; + .map(|min_idx| min_idx << SAPLING_SHARD_HEIGHT); + let max_tree_size = row + .get::<_, Option>(1)? + .map(|max_idx| (max_idx + 1) << SAPLING_SHARD_HEIGHT); + Ok(start_size.or(min_tree_size).zip(max_tree_size).map( + |(min_tree_size, max_tree_size)| { Ratio::new( scanned_count.unwrap_or(0), max_tree_size - min_tree_size, ) - })) + }, + )) }, ) .optional()? @@ -961,22 +988,38 @@ impl ScanProgress for SubtreeScanProgress { ) .map_err(SqliteClientError::from) } else { - let start_height = birthday_height; // Compute the starting number of notes directly from the blocks table - let start_size = conn.query_row( - "SELECT MAX(orchard_commitment_tree_size) - FROM blocks - WHERE height <= :start_height", - named_params![":start_height": u32::from(start_height)], - |row| row.get::<_, Option>(0), - )?; + let start_size = conn + .query_row( + "SELECT birthday_orchard_tree_size + FROM accounts + WHERE birthday_height = :birthday_height", + named_params![":birthday_height": u32::from(birthday_height)], + |row| row.get::<_, Option>(0), + ) + .optional()? + .flatten() + .map(Ok) + .or_else(|| { + conn.query_row( + "SELECT MAX(orchard_commitment_tree_size - orchard_action_count) + FROM blocks + WHERE height <= :start_height", + named_params![":start_height": u32::from(birthday_height)], + |row| row.get::<_, Option>(0), + ) + .optional() + .map(|opt| opt.flatten()) + .transpose() + }) + .transpose()?; // Compute the total blocks scanned so far above the starting height let scanned_count = conn.query_row( "SELECT SUM(orchard_action_count) FROM blocks WHERE height > :start_height", - named_params![":start_height": u32::from(start_height)], + named_params![":start_height": u32::from(birthday_height)], |row| row.get::<_, Option>(0), )?; @@ -992,22 +1035,22 @@ impl ScanProgress for SubtreeScanProgress { FROM orchard_tree_shards WHERE subtree_end_height > :start_height OR subtree_end_height IS NULL", - named_params![":start_height": u32::from(start_height)], + named_params![":start_height": u32::from(birthday_height)], |row| { let min_tree_size = row .get::<_, Option>(0)? - .map(|min| min << ORCHARD_SHARD_HEIGHT); - let max_idx = row.get::<_, Option>(1)?; - Ok(start_size - .or(min_tree_size) - .zip(max_idx) - .map(|(min_tree_size, max)| { - let max_tree_size = (max + 1) << ORCHARD_SHARD_HEIGHT; + .map(|min_idx| min_idx << ORCHARD_SHARD_HEIGHT); + let max_tree_size = row + .get::<_, Option>(1)? + .map(|max_idx| (max_idx + 1) << ORCHARD_SHARD_HEIGHT); + Ok(start_size.or(min_tree_size).zip(max_tree_size).map( + |(min_tree_size, max_tree_size)| { Ratio::new( scanned_count.unwrap_or(0), max_tree_size - min_tree_size, ) - })) + }, + )) }, ) .optional()? @@ -3060,6 +3103,7 @@ mod tests { not_our_value, 0, 0, + false, ); let (mid_height, _, _) = st.generate_next_block(¬_our_key, AddressType::DefaultExternal, not_our_value); diff --git a/zcash_client_sqlite/src/wallet/init.rs b/zcash_client_sqlite/src/wallet/init.rs index 7ef261d30..cf70543ac 100644 --- a/zcash_client_sqlite/src/wallet/init.rs +++ b/zcash_client_sqlite/src/wallet/init.rs @@ -370,8 +370,23 @@ mod tests { sapling_fvk_item_cache BLOB, p2pkh_fvk_item_cache BLOB, birthday_height INTEGER NOT NULL, + birthday_sapling_tree_size INTEGER, + birthday_orchard_tree_size INTEGER, recover_until_height INTEGER, - CHECK ( (account_kind = 0 AND hd_seed_fingerprint IS NOT NULL AND hd_account_index IS NOT NULL AND ufvk IS NOT NULL) OR (account_kind = 1 AND hd_seed_fingerprint IS NULL AND hd_account_index IS NULL) ) + CHECK ( + ( + account_kind = 0 + AND hd_seed_fingerprint IS NOT NULL + AND hd_account_index IS NOT NULL + AND ufvk IS NOT NULL + ) + OR + ( + account_kind = 1 + AND hd_seed_fingerprint IS NULL + AND hd_account_index IS NULL + ) + ) )"#, r#"CREATE TABLE "addresses" ( account_id INTEGER NOT NULL, diff --git a/zcash_client_sqlite/src/wallet/init/migrations.rs b/zcash_client_sqlite/src/wallet/init/migrations.rs index 9ee86f7c7..0771c737b 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations.rs @@ -56,10 +56,10 @@ pub(super) fn all_migrations( // v_sapling_shard_unscanned_ranges \ | v_tx_outputs_use_legacy_false // | \ | | // wallet_summaries \ | v_transactions_shielding_balance - // \ | | - // \ | v_transactions_note_uniqueness - // \ | / - // full_account_ids + // \ \ | | + // \ \ | v_transactions_note_uniqueness + // \ \ | / + // -------------------- full_account_ids // | // orchard_received_notes vec![ diff --git a/zcash_client_sqlite/src/wallet/init/migrations/full_account_ids.rs b/zcash_client_sqlite/src/wallet/init/migrations/full_account_ids.rs index 59a5158fd..b08d092e4 100644 --- a/zcash_client_sqlite/src/wallet/init/migrations/full_account_ids.rs +++ b/zcash_client_sqlite/src/wallet/init/migrations/full_account_ids.rs @@ -1,7 +1,7 @@ use std::{collections::HashSet, rc::Rc}; use crate::wallet::{account_kind_code, init::WalletMigrationError}; -use rusqlite::{named_params, Transaction}; +use rusqlite::{named_params, OptionalExtension, Transaction}; use schemer_rusqlite::RusqliteMigration; use secrecy::{ExposeSecret, SecretVec}; use uuid::Uuid; @@ -10,7 +10,9 @@ use zcash_keys::keys::UnifiedFullViewingKey; use zcash_primitives::consensus; use zip32::fingerprint::SeedFingerprint; -use super::{add_account_birthdays, receiving_key_scopes, v_transactions_note_uniqueness}; +use super::{ + add_account_birthdays, receiving_key_scopes, v_transactions_note_uniqueness, wallet_summaries, +}; /// The migration that switched from presumed seed-derived account IDs to supporting /// HD accounts and all sorts of imported keys. @@ -31,6 +33,7 @@ impl schemer::Migration for Migration

{ receiving_key_scopes::MIGRATION_ID, add_account_birthdays::MIGRATION_ID, v_transactions_note_uniqueness::MIGRATION_ID, + wallet_summaries::MIGRATION_ID, ] .into_iter() .collect() @@ -50,8 +53,8 @@ impl RusqliteMigration for Migration

{ account_index: zip32::AccountId::ZERO, }); let account_kind_imported = account_kind_code(AccountSource::Imported); - transaction.execute_batch( - &format!(r#" + transaction.execute_batch(&format!( + r#" PRAGMA foreign_keys = OFF; PRAGMA legacy_alter_table = ON; @@ -66,18 +69,29 @@ impl RusqliteMigration for Migration

{ sapling_fvk_item_cache BLOB, p2pkh_fvk_item_cache BLOB, birthday_height INTEGER NOT NULL, + birthday_sapling_tree_size INTEGER, + birthday_orchard_tree_size INTEGER, recover_until_height INTEGER, CHECK ( - (account_kind = {account_kind_derived} AND hd_seed_fingerprint IS NOT NULL AND hd_account_index IS NOT NULL AND ufvk IS NOT NULL) - OR - (account_kind = {account_kind_imported} AND hd_seed_fingerprint IS NULL AND hd_account_index IS NULL) + ( + account_kind = {account_kind_derived} + AND hd_seed_fingerprint IS NOT NULL + AND hd_account_index IS NOT NULL + AND ufvk IS NOT NULL + ) + OR + ( + account_kind = {account_kind_imported} + AND hd_seed_fingerprint IS NULL + AND hd_account_index IS NULL + ) ) ); CREATE UNIQUE INDEX hd_account ON accounts_new (hd_seed_fingerprint, hd_account_index); CREATE UNIQUE INDEX accounts_uivk ON accounts_new (uivk); CREATE UNIQUE INDEX accounts_ufvk ON accounts_new (ufvk); - "#), - )?; + "# + ))?; // We require the seed *if* there are existing accounts in the table. if transaction.query_row("SELECT COUNT(*) FROM accounts", [], |row| { @@ -158,19 +172,40 @@ impl RusqliteMigration for Migration

{ #[cfg(not(feature = "transparent-inputs"))] let transparent_item: Option> = None; + // Get the tree sizes for the birthday height from the blocks table, if + // available. + let (birthday_sapling_tree_size, birthday_orchard_tree_size) = transaction + .query_row( + "SELECT sapling_commitment_tree_size - sapling_output_count, + orchard_commitment_tree_size - orchard_action_count + FROM blocks + WHERE height = :birthday_height", + named_params![":birthday_height": birthday_height], + |row| { + Ok(row + .get::<_, Option>(0)? + .zip(row.get::<_, Option>(1)?)) + }, + ) + .optional()? + .flatten() + .map_or((None, None), |(s, o)| (Some(s), Some(o))); + transaction.execute( r#" INSERT INTO accounts_new ( id, account_kind, hd_seed_fingerprint, hd_account_index, ufvk, uivk, orchard_fvk_item_cache, sapling_fvk_item_cache, p2pkh_fvk_item_cache, - birthday_height, recover_until_height + birthday_height, birthday_sapling_tree_size, birthday_orchard_tree_size, + recover_until_height ) VALUES ( :account_id, :account_kind, :seed_id, :account_index, :ufvk, :uivk, :orchard_fvk_item_cache, :sapling_fvk_item_cache, :p2pkh_fvk_item_cache, - :birthday_height, :recover_until_height + :birthday_height, :birthday_sapling_tree_size, :birthday_orchard_tree_size, + :recover_until_height ); "#, named_params![ @@ -184,6 +219,8 @@ impl RusqliteMigration for Migration

{ ":sapling_fvk_item_cache": ufvk_parsed.sapling().map(|k| k.to_bytes()), ":p2pkh_fvk_item_cache": transparent_item, ":birthday_height": birthday_height, + ":birthday_sapling_tree_size": birthday_sapling_tree_size, + ":birthday_orchard_tree_size": birthday_orchard_tree_size, ":recover_until_height": recover_until_height, ], )?; diff --git a/zcash_client_sqlite/src/wallet/scanning.rs b/zcash_client_sqlite/src/wallet/scanning.rs index c859c5eb7..0c3750174 100644 --- a/zcash_client_sqlite/src/wallet/scanning.rs +++ b/zcash_client_sqlite/src/wallet/scanning.rs @@ -594,7 +594,6 @@ pub(crate) mod tests { consensus::{BlockHeight, NetworkUpgrade, Parameters}, transaction::components::amount::NonNegativeAmount, }; - use zcash_protocol::ShieldedProtocol; use crate::{ error::SqliteClientError, @@ -610,10 +609,7 @@ pub(crate) mod tests { }; #[cfg(feature = "orchard")] - use { - crate::wallet::orchard::tests::OrchardPoolTester, orchard::tree::MerkleHashOrchard, - zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT, - }; + use {crate::wallet::orchard::tests::OrchardPoolTester, orchard::tree::MerkleHashOrchard}; #[test] fn sapling_scan_complete() { @@ -703,6 +699,7 @@ pub(crate) mod tests { value, initial_sapling_tree_size, initial_orchard_tree_size, + false, ); for _ in 1..=10 { @@ -1111,6 +1108,7 @@ pub(crate) mod tests { NonNegativeAmount::const_from_u64(10000), frontier_tree_size + 10, frontier_tree_size + 10, + false, ); st.scan_cached_blocks(max_scanned, 1); @@ -1188,9 +1186,7 @@ pub(crate) mod tests { assert_eq!(actual, expected); } - // FIXME: This requires fixes to the test framework. #[test] - #[cfg(feature = "orchard")] fn sapling_update_chain_tip_stable_max_scanned() { update_chain_tip_stable_max_scanned::(); } @@ -1201,36 +1197,74 @@ pub(crate) mod tests { update_chain_tip_stable_max_scanned::(); } - // FIXME: This requires fixes to the test framework. - #[allow(dead_code)] fn update_chain_tip_stable_max_scanned() { use ScanPriority::*; - // Use a non-zero birthday offset because Sapling and NU5 are activated at the same height. - let (mut st, dfvk, birthday, sap_active) = - test_with_nu5_birthday_offset::(76, BlockHash([0; 32])); - // Set up the following situation: // // prior_tip new_tip // |<--- 500 --->|<- 20 ->|<-- 50 -->|<- 20 ->| // wallet_birthday max_scanned last_shard_start // - let max_scanned = birthday.height() + 500; - let prior_tip = max_scanned + 20; + let birthday_offset = 76; + let birthday_prior_block_hash = BlockHash([0; 32]); + // 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_block_cache() + .with_initial_chain_state(|rng, network| { + let birthday_height = + network.activation_height(NetworkUpgrade::Nu5).unwrap() + birthday_offset; - // Set up some shard root history before the wallet birthday. - let second_to_last_shard_start = birthday.height() - 1000; - T::put_subtree_roots( - &mut st, - 0, - &[CommitmentTreeRoot::from_parts( - second_to_last_shard_start, - // fake a hash, the value doesn't matter - T::empty_tree_leaf(), - )], - ) - .unwrap(); + // 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 - 10, 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 - 10, root)) + .collect::>(); + + InitialChainState { + chain_state: ChainState::new( + birthday_height - 1, + birthday_prior_block_hash, + 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 account = st.test_account().cloned().unwrap(); + let dfvk = T::test_account_fvk(&st); + let birthday = account.birthday(); + let sap_active = st.sapling_activation_height(); // We have scan ranges and a subtree, but have scanned no blocks. let summary = st.get_wallet_summary(1); @@ -1238,67 +1272,56 @@ pub(crate) mod tests { // Set up prior chain state. This simulates us having imported a wallet // with a birthday 520 blocks below the chain tip. + let max_scanned = birthday.height() + 500; + let prior_tip = max_scanned + 20; st.wallet_mut().update_chain_tip(prior_tip).unwrap(); // Verify that the suggested scan ranges match what is expected. let expected = vec![ scan_range(birthday.height().into()..(prior_tip + 1).into(), ChainTip), - scan_range(sap_active..birthday.height().into(), Ignored), + scan_range(sap_active.into()..birthday.height().into(), Ignored), ]; let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap(); assert_eq!(actual, expected); - // Now, scan the max scanned block. - let initial_sapling_tree_size = birthday - .sapling_frontier() - .value() - .map(|f| u64::from(f.position() + 1)) - .unwrap_or(0) - .try_into() - .unwrap(); - #[cfg(feature = "orchard")] - let initial_orchard_tree_size = birthday - .orchard_frontier() - .value() - .map(|f| u64::from(f.position() + 1)) - .unwrap_or(0) - .try_into() - .unwrap(); - #[cfg(not(feature = "orchard"))] - let initial_orchard_tree_size = 0; + // Simulate that in the blocks between the wallet birthday and the max_scanned height, + // there are 10 Sapling notes and 10 Orchard notes created on the chain. st.generate_block_at( max_scanned, - BlockHash([0u8; 32]), + BlockHash([1; 32]), &dfvk, AddressType::DefaultExternal, NonNegativeAmount::const_from_u64(10000), - initial_sapling_tree_size, - initial_orchard_tree_size, + frontier_tree_size + 10, + frontier_tree_size + 10, + false, ); st.scan_cached_blocks(max_scanned, 1); // We have scanned a block, so we now have a starting tree position, 500 blocks above the // wallet birthday but before the end of the shard. let summary = st.get_wallet_summary(1); - assert_eq!(summary.as_ref().map(|s| T::next_subtree_index(s)), Some(0),); + assert_eq!(summary.as_ref().map(|s| T::next_subtree_index(s)), Some(0)); // Progress denominator depends on which pools are enabled (which changes the - // initial tree states in `test_with_nu5_birthday_offset`). - let expected_denom = 1 << SAPLING_SHARD_HEIGHT; + // initial tree states). Here we compute the denominator based upon the fact that + // the trees are the same size at present. + let expected_denom = (1 << SAPLING_SHARD_HEIGHT) * 2 - frontier_tree_size; #[cfg(feature = "orchard")] - let expected_denom = expected_denom + (1 << ORCHARD_SHARD_HEIGHT); + let expected_denom = expected_denom * 2; assert_eq!( summary.and_then(|s| s.scan_progress()), - Some(Ratio::new(1, expected_denom)) + Some(Ratio::new(1, u64::from(expected_denom))) ); // Now simulate shutting down, and then restarting 70 blocks later, after a shard - // has been completed. + // has been completed in one pool. This shard will have index 2, as our birthday + // was in shard 1. let last_shard_start = prior_tip + 50; T::put_subtree_roots( &mut st, - 0, + 2, &[CommitmentTreeRoot::from_parts( last_shard_start, // fake a hash, the value doesn't matter @@ -1307,6 +1330,36 @@ pub(crate) mod tests { ) .unwrap(); + { + let mut shard_stmt = st + .wallet_mut() + .conn + .prepare("SELECT shard_index, subtree_end_height FROM sapling_tree_shards") + .unwrap(); + (shard_stmt + .query_and_then::<_, rusqlite::Error, _, _>([], |row| { + Ok((row.get::<_, u32>(0)?, row.get::<_, Option>(1)?)) + }) + .unwrap() + .collect::, _>>()) + .unwrap(); + } + + { + let mut shard_stmt = st + .wallet_mut() + .conn + .prepare("SELECT shard_index, subtree_end_height FROM orchard_tree_shards") + .unwrap(); + (shard_stmt + .query_and_then::<_, rusqlite::Error, _, _>([], |row| { + Ok((row.get::<_, u32>(0)?, row.get::<_, Option>(1)?)) + }) + .unwrap() + .collect::, _>>()) + .unwrap(); + } + let new_tip = last_shard_start + 20; st.wallet_mut().update_chain_tip(new_tip).unwrap(); let chain_end = u32::from(new_tip + 1); @@ -1320,26 +1373,20 @@ pub(crate) mod tests { // The max scanned block itself is left as-is. scan_range(max_scanned.into()..(max_scanned + 1).into(), Scanned), // The range below the second-to-last shard is ignored. - scan_range(sap_active..birthday.height().into(), Ignored), + scan_range(sap_active.into()..birthday.height().into(), Ignored), ]; let actual = suggest_scan_ranges(&st.wallet().conn, Ignored).unwrap(); assert_eq!(actual, expected); - // We've crossed a subtree boundary, and so still only have one scanned note but have two - // shards worth of notes to scan. - let expected_denom = expected_denom - + match T::SHIELDED_PROTOCOL { - ShieldedProtocol::Sapling => 1 << SAPLING_SHARD_HEIGHT, - #[cfg(feature = "orchard")] - ShieldedProtocol::Orchard => 1 << ORCHARD_SHARD_HEIGHT, - #[cfg(not(feature = "orchard"))] - ShieldedProtocol::Orchard => unreachable!(), - }; + // We've crossed a subtree boundary, but only in one pool. We still only have one scanned + // note but in the pool where we crossed the subtree boundary we have two shards worth of + // notes to scan. + let expected_denom = expected_denom + (1 << 16); let summary = st.get_wallet_summary(1); assert_eq!( summary.and_then(|s| s.scan_progress()), - Some(Ratio::new(1, expected_denom)) + Some(Ratio::new(1, u64::from(expected_denom))) ); }