From a18120317935ce3d479698cecc03188d5b0c5df3 Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Tue, 25 Aug 2020 15:20:12 -0600 Subject: [PATCH] Move related functions into the same modules. --- zcash_client_sqlite/src/chain.rs | 321 ++++++++++++------ zcash_client_sqlite/src/init.rs | 4 +- zcash_client_sqlite/src/lib.rs | 31 +- zcash_client_sqlite/src/scan.rs | 243 ------------- zcash_client_sqlite/src/transact.rs | 2 +- .../src/{query.rs => wallet.rs} | 109 +++++- 6 files changed, 334 insertions(+), 376 deletions(-) delete mode 100644 zcash_client_sqlite/src/scan.rs rename zcash_client_sqlite/src/{query.rs => wallet.rs} (77%) diff --git a/zcash_client_sqlite/src/chain.rs b/zcash_client_sqlite/src/chain.rs index aa2e6cd57..2f37b7ca4 100644 --- a/zcash_client_sqlite/src/chain.rs +++ b/zcash_client_sqlite/src/chain.rs @@ -18,10 +18,9 @@ //! } //! }; //! -//! use zcash_client_sqlite::{ -//! DataConnection, +//! use crate::{ //! CacheConnection, -//! chain::{rewind_to_height}, +//! wallet::{rewind_to_height}, //! }; //! //! let network = Network::TestNetwork; @@ -73,20 +72,17 @@ //! scan_cached_blocks(&network, &db_cache, &db_data, None); //! ``` use protobuf::parse_from_bytes; -use rusqlite::{OptionalExtension, NO_PARAMS}; -use zcash_primitives::{ - block::BlockHash, - consensus::{self, BlockHeight, NetworkUpgrade}, - transaction::TxId, -}; +use rusqlite::types::ToSql; + +use zcash_primitives::{block::BlockHash, consensus::BlockHeight}; use zcash_client_backend::{ data_api::error::{ChainInvalid, Error}, proto::compact_formats::CompactBlock, }; -use crate::{error::SqliteClientError, CacheConnection, DataConnection}; +use crate::{error::SqliteClientError, CacheConnection}; struct CompactBlockRow { height: BlockHeight, @@ -148,105 +144,37 @@ where Ok(Some(current_block.hash())) } -pub fn block_height_extrema( - conn: &DataConnection, -) -> Result, rusqlite::Error> { - conn.0 - .query_row( - "SELECT MIN(height), MAX(height) FROM blocks", - NO_PARAMS, - |row| { - let min_height: u32 = row.get(0)?; - let max_height: u32 = row.get(1)?; - Ok(Some((min_height.into(), max_height.into()))) - }, - ) - //.optional() doesn't work here because a failed aggregate function - //produces a runtime error, not an empty set of rows. - .or(Ok(None)) -} +pub fn with_cached_blocks( + cache: &CacheConnection, + from_height: BlockHeight, + limit: Option, + mut with_row: F, +) -> Result<(), SqliteClientError> +where + F: FnMut(BlockHeight, CompactBlock) -> Result<(), SqliteClientError>, +{ + // Fetch the CompactBlocks we need to scan + let mut stmt_blocks = cache.0.prepare( + "SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC LIMIT ?", + )?; + let rows = stmt_blocks.query_map( + &[ + u32::from(from_height).to_sql()?, + limit.unwrap_or(u32::max_value()).to_sql()?, + ], + |row| { + Ok(CompactBlockRow { + height: BlockHeight::from_u32(row.get(0)?), + data: row.get(1)?, + }) + }, + )?; -pub fn get_tx_height( - conn: &DataConnection, - txid: TxId, -) -> Result, rusqlite::Error> { - conn.0 - .query_row( - "SELECT block FROM transactions WHERE txid = ?", - &[txid.0.to_vec()], - |row| row.get(0).map(u32::into), - ) - .optional() -} - -pub fn get_block_hash( - conn: &DataConnection, - block_height: BlockHeight, -) -> Result, rusqlite::Error> { - conn.0 - .query_row( - "SELECT hash FROM blocks WHERE height = ?", - &[u32::from(block_height)], - |row| { - let row_data = row.get::<_, Vec<_>>(0)?; - Ok(BlockHash::from_slice(&row_data)) - }, - ) - .optional() -} - -/// Rewinds the database to the given height. -/// -/// If the requested height is greater than or equal to the height of the last scanned -/// block, this function does nothing. -pub fn rewind_to_height( - conn: &DataConnection, - parameters: &P, - block_height: BlockHeight, -) -> Result<(), SqliteClientError> { - let sapling_activation_height = parameters - .activation_height(NetworkUpgrade::Sapling) - .ok_or(SqliteClientError(Error::SaplingNotActive))?; - - // Recall where we synced up to previously. - // If we have never synced, use Sapling activation height. - let last_scanned_height = - conn.0 - .query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| { - row.get(0) - .map(u32::into) - .or(Ok(sapling_activation_height - 1)) - })?; - - if block_height >= last_scanned_height { - // Nothing to do. - return Ok(()); + for row_result in rows { + let row = row_result?; + with_row(row.height, parse_from_bytes(&row.data)?)?; } - // Start an SQL transaction for rewinding. - conn.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?; - - // Decrement witnesses. - conn.0.execute( - "DELETE FROM sapling_witnesses WHERE block > ?", - &[u32::from(block_height)], - )?; - - // Un-mine transactions. - conn.0.execute( - "UPDATE transactions SET block = NULL, tx_index = NULL WHERE block > ?", - &[u32::from(block_height)], - )?; - - // Now that they aren't depended on, delete scanned blocks. - conn.0.execute( - "DELETE FROM blocks WHERE height > ?", - &[u32::from(block_height)], - )?; - - // Commit the SQL transaction, rewinding atomically. - conn.0.execute("COMMIT", NO_PARAMS)?; - Ok(()) } @@ -263,18 +191,19 @@ mod tests { use zcash_client_backend::data_api::{ chain::{scan_cached_blocks, validate_combined_chain}, - error::Error, + error::{ChainInvalid, Error}, }; use crate::{ init::{init_accounts_table, init_cache_database, init_data_database}, - query::get_balance, - tests::{self, fake_compact_block, insert_into_cache, sapling_activation_height}, - AccountId, CacheConnection, DataConnection, + tests::{ + self, fake_compact_block, fake_compact_block_spending, insert_into_cache, + sapling_activation_height, + }, + wallet::{get_balance, rewind_to_height}, + AccountId, CacheConnection, DataConnection, NoteId, }; - use super::rewind_to_height; - #[test] fn valid_chain_states() { let cache_file = NamedTempFile::new().unwrap(); @@ -511,4 +440,172 @@ mod tests { // Account balance should again reflect both received notes assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value + value2); } + + #[test] + fn scan_cached_blocks_requires_sequential_blocks() { + let cache_file = NamedTempFile::new().unwrap(); + let db_cache = CacheConnection(Connection::open(cache_file.path()).unwrap()); + init_cache_database(&db_cache).unwrap(); + + let data_file = NamedTempFile::new().unwrap(); + let db_data = DataConnection(Connection::open(data_file.path()).unwrap()); + init_data_database(&db_data).unwrap(); + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap(); + + // Create a block with height SAPLING_ACTIVATION_HEIGHT + let value = Amount::from_u64(50000).unwrap(); + let (cb1, _) = fake_compact_block( + sapling_activation_height(), + BlockHash([0; 32]), + extfvk.clone(), + value, + ); + insert_into_cache(&db_cache, &cb1); + scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); + assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); + + // We cannot scan a block of height SAPLING_ACTIVATION_HEIGHT + 2 next + let (cb2, _) = fake_compact_block( + sapling_activation_height() + 1, + cb1.hash(), + extfvk.clone(), + value, + ); + let (cb3, _) = fake_compact_block( + sapling_activation_height() + 2, + cb2.hash(), + extfvk.clone(), + value, + ); + insert_into_cache(&db_cache, &cb3); + match scan_cached_blocks(&tests::network(), &db_cache, &db_data, None) { + Ok(_) => panic!("Should have failed"), + Err(e) => { + assert_eq!( + e.to_string(), + ChainInvalid::block_height_mismatch::( + sapling_activation_height() + 1, + sapling_activation_height() + 2 + ) + .to_string() + ); + } + } + + // If we add a block of height SAPLING_ACTIVATION_HEIGHT + 1, we can now scan both + insert_into_cache(&db_cache, &cb2); + scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); + assert_eq!( + get_balance(&db_data, AccountId(0)).unwrap(), + Amount::from_u64(150_000).unwrap() + ); + } + + #[test] + fn scan_cached_blocks_finds_received_notes() { + let cache_file = NamedTempFile::new().unwrap(); + let db_cache = CacheConnection(Connection::open(cache_file.path()).unwrap()); + init_cache_database(&db_cache).unwrap(); + + let data_file = NamedTempFile::new().unwrap(); + let db_data = DataConnection(Connection::open(data_file.path()).unwrap()); + init_data_database(&db_data).unwrap(); + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap(); + + // Account balance should be zero + assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero()); + + // Create a fake CompactBlock sending value to the address + let value = Amount::from_u64(5).unwrap(); + let (cb, _) = fake_compact_block( + sapling_activation_height(), + BlockHash([0; 32]), + extfvk.clone(), + value, + ); + insert_into_cache(&db_cache, &cb); + + // Scan the cache + scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); + + // Account balance should reflect the received note + assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); + + // Create a second fake CompactBlock sending more value to the address + let value2 = Amount::from_u64(7).unwrap(); + let (cb2, _) = + fake_compact_block(sapling_activation_height() + 1, cb.hash(), extfvk, value2); + insert_into_cache(&db_cache, &cb2); + + // Scan the cache again + scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); + + // Account balance should reflect both received notes + assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value + value2); + } + + #[test] + fn scan_cached_blocks_finds_change_notes() { + let cache_file = NamedTempFile::new().unwrap(); + let db_cache = CacheConnection(Connection::open(cache_file.path()).unwrap()); + init_cache_database(&db_cache).unwrap(); + + let data_file = NamedTempFile::new().unwrap(); + let db_data = DataConnection(Connection::open(data_file.path()).unwrap()); + init_data_database(&db_data).unwrap(); + + // Add an account to the wallet + let extsk = ExtendedSpendingKey::master(&[]); + let extfvk = ExtendedFullViewingKey::from(&extsk); + init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap(); + + // Account balance should be zero + assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero()); + + // Create a fake CompactBlock sending value to the address + let value = Amount::from_u64(5).unwrap(); + let (cb, nf) = fake_compact_block( + sapling_activation_height(), + BlockHash([0; 32]), + extfvk.clone(), + value, + ); + insert_into_cache(&db_cache, &cb); + + // Scan the cache + scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); + + // Account balance should reflect the received note + assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); + + // Create a second fake CompactBlock spending value from the address + let extsk2 = ExtendedSpendingKey::master(&[0]); + let to2 = extsk2.default_address().unwrap().1; + let value2 = Amount::from_u64(2).unwrap(); + insert_into_cache( + &db_cache, + &fake_compact_block_spending( + sapling_activation_height() + 1, + cb.hash(), + (nf, value), + extfvk, + to2, + value2, + ), + ); + + // Scan the cache again + scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); + + // Account balance should equal the change + assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value - value2); + } } diff --git a/zcash_client_sqlite/src/init.rs b/zcash_client_sqlite/src/init.rs index f6266c5a8..c5e3f4ab6 100644 --- a/zcash_client_sqlite/src/init.rs +++ b/zcash_client_sqlite/src/init.rs @@ -165,7 +165,7 @@ pub fn init_data_database(db_data: &DataConnection) -> Result<(), rusqlite::Erro /// init_accounts_table(&db_data, &Network::TestNetwork, &extfvks).unwrap(); /// ``` /// -/// [`get_address`]: crate::query::get_address +/// [`get_address`]: crate::wallet::get_address /// [`scan_cached_blocks`]: crate::scan::scan_cached_blocks /// [`create_to_address`]: crate::transact::create_to_address pub fn init_accounts_table( @@ -272,7 +272,7 @@ mod tests { zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, }; - use crate::{query::get_address, tests, AccountId, DataConnection}; + use crate::{tests, wallet::get_address, AccountId, DataConnection}; use super::{init_accounts_table, init_blocks_table, init_data_database}; diff --git a/zcash_client_sqlite/src/lib.rs b/zcash_client_sqlite/src/lib.rs index 1f42ccab0..588ccea50 100644 --- a/zcash_client_sqlite/src/lib.rs +++ b/zcash_client_sqlite/src/lib.rs @@ -54,9 +54,8 @@ use crate::error::SqliteClientError; pub mod chain; pub mod error; pub mod init; -pub mod query; -pub mod scan; pub mod transact; +pub mod wallet; #[derive(Debug, Copy, Clone)] pub struct NoteId(pub i64); @@ -103,15 +102,15 @@ impl<'a> DBOps for &'a DataConnection { } fn block_height_extrema(&self) -> Result, Self::Error> { - chain::block_height_extrema(self).map_err(SqliteClientError::from) + wallet::block_height_extrema(self).map_err(SqliteClientError::from) } fn get_block_hash(&self, block_height: BlockHeight) -> Result, Self::Error> { - chain::get_block_hash(self, block_height).map_err(SqliteClientError::from) + wallet::get_block_hash(self, block_height).map_err(SqliteClientError::from) } fn get_tx_height(&self, txid: TxId) -> Result, Self::Error> { - chain::get_tx_height(self, txid).map_err(SqliteClientError::from) + wallet::get_tx_height(self, txid).map_err(SqliteClientError::from) } fn rewind_to_height( @@ -119,7 +118,7 @@ impl<'a> DBOps for &'a DataConnection { parameters: &P, block_height: BlockHeight, ) -> Result<(), Self::Error> { - chain::rewind_to_height(self, parameters, block_height) + wallet::rewind_to_height(self, parameters, block_height) } fn get_address( @@ -127,7 +126,7 @@ impl<'a> DBOps for &'a DataConnection { params: &P, account: AccountId, ) -> Result, Self::Error> { - query::get_address(self, params, account) + wallet::get_address(self, params, account) } fn get_account_extfvks( @@ -150,47 +149,47 @@ impl<'a> DBOps for &'a DataConnection { } fn get_balance(&self, account: AccountId) -> Result { - query::get_balance(self, account) + wallet::get_balance(self, account) } fn get_verified_balance(&self, account: AccountId) -> Result { - query::get_verified_balance(self, account) + wallet::get_verified_balance(self, account) } fn get_received_memo_as_utf8( &self, id_note: Self::NoteRef, ) -> Result, Self::Error> { - query::get_received_memo_as_utf8(self, id_note) + wallet::get_received_memo_as_utf8(self, id_note) } fn get_sent_memo_as_utf8(&self, id_note: Self::NoteRef) -> Result, Self::Error> { - query::get_sent_memo_as_utf8(self, id_note) + wallet::get_sent_memo_as_utf8(self, id_note) } fn get_extended_full_viewing_keys( &self, params: &P, ) -> Result, Self::Error> { - query::get_extended_full_viewing_keys(self, params) + wallet::get_extended_full_viewing_keys(self, params) } fn get_commitment_tree( &self, block_height: BlockHeight, ) -> Result>, Self::Error> { - query::get_commitment_tree(self, block_height) + wallet::get_commitment_tree(self, block_height) } fn get_witnesses( &self, block_height: BlockHeight, ) -> Result)>, Self::Error> { - query::get_witnesses(self, block_height) + wallet::get_witnesses(self, block_height) } fn get_nullifiers(&self) -> Result, AccountId)>, Self::Error> { - query::get_nullifiers(self) + wallet::get_nullifiers(self) } fn get_update_ops(&self) -> Result { @@ -558,7 +557,7 @@ impl CacheOps for CacheConnection { where F: FnMut(BlockHeight, CompactBlock) -> Result<(), Self::Error>, { - scan::with_cached_blocks(self, from_height, limit, with_row) + chain::with_cached_blocks(self, from_height, limit, with_row) } } diff --git a/zcash_client_sqlite/src/scan.rs b/zcash_client_sqlite/src/scan.rs deleted file mode 100644 index dbc4aa773..000000000 --- a/zcash_client_sqlite/src/scan.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! Functions for scanning the chain and extracting relevant information. - -use protobuf::parse_from_bytes; - -use rusqlite::types::ToSql; - -use zcash_primitives::consensus::BlockHeight; - -use zcash_client_backend::proto::compact_formats::CompactBlock; - -use crate::{error::SqliteClientError, CacheConnection}; - -struct CompactBlockRow { - height: BlockHeight, - data: Vec, -} - -pub fn with_cached_blocks( - cache: &CacheConnection, - from_height: BlockHeight, - limit: Option, - mut with_row: F, -) -> Result<(), SqliteClientError> -where - F: FnMut(BlockHeight, CompactBlock) -> Result<(), SqliteClientError>, -{ - // Fetch the CompactBlocks we need to scan - let mut stmt_blocks = cache.0.prepare( - "SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC LIMIT ?", - )?; - let rows = stmt_blocks.query_map( - &[ - u32::from(from_height).to_sql()?, - limit.unwrap_or(u32::max_value()).to_sql()?, - ], - |row| { - Ok(CompactBlockRow { - height: BlockHeight::from_u32(row.get(0)?), - data: row.get(1)?, - }) - }, - )?; - - for row_result in rows { - let row = row_result?; - with_row(row.height, parse_from_bytes(&row.data)?)?; - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use rusqlite::Connection; - - use tempfile::NamedTempFile; - - use zcash_primitives::{ - block::BlockHash, - transaction::components::Amount, - zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}, - }; - - use zcash_client_backend::data_api::{chain::scan_cached_blocks, error::ChainInvalid}; - - use crate::{ - init::{init_accounts_table, init_cache_database, init_data_database}, - query::get_balance, - tests::{ - self, fake_compact_block, fake_compact_block_spending, insert_into_cache, - sapling_activation_height, - }, - AccountId, CacheConnection, DataConnection, NoteId, - }; - - #[test] - fn scan_cached_blocks_requires_sequential_blocks() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = CacheConnection(Connection::open(cache_file.path()).unwrap()); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = DataConnection(Connection::open(data_file.path()).unwrap()); - init_data_database(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap(); - - // Create a block with height SAPLING_ACTIVATION_HEIGHT - let value = Amount::from_u64(50000).unwrap(); - let (cb1, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - value, - ); - insert_into_cache(&db_cache, &cb1); - scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); - - // We cannot scan a block of height SAPLING_ACTIVATION_HEIGHT + 2 next - let (cb2, _) = fake_compact_block( - sapling_activation_height() + 1, - cb1.hash(), - extfvk.clone(), - value, - ); - let (cb3, _) = fake_compact_block( - sapling_activation_height() + 2, - cb2.hash(), - extfvk.clone(), - value, - ); - insert_into_cache(&db_cache, &cb3); - match scan_cached_blocks(&tests::network(), &db_cache, &db_data, None) { - Ok(_) => panic!("Should have failed"), - Err(e) => { - assert_eq!( - e.to_string(), - ChainInvalid::block_height_mismatch::( - sapling_activation_height() + 1, - sapling_activation_height() + 2 - ) - .to_string() - ); - } - } - - // If we add a block of height SAPLING_ACTIVATION_HEIGHT + 1, we can now scan both - insert_into_cache(&db_cache, &cb2); - scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); - assert_eq!( - get_balance(&db_data, AccountId(0)).unwrap(), - Amount::from_u64(150_000).unwrap() - ); - } - - #[test] - fn scan_cached_blocks_finds_received_notes() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = CacheConnection(Connection::open(cache_file.path()).unwrap()); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = DataConnection(Connection::open(data_file.path()).unwrap()); - init_data_database(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap(); - - // Account balance should be zero - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero()); - - // Create a fake CompactBlock sending value to the address - let value = Amount::from_u64(5).unwrap(); - let (cb, _) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - value, - ); - insert_into_cache(&db_cache, &cb); - - // Scan the cache - scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); - - // Account balance should reflect the received note - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); - - // Create a second fake CompactBlock sending more value to the address - let value2 = Amount::from_u64(7).unwrap(); - let (cb2, _) = - fake_compact_block(sapling_activation_height() + 1, cb.hash(), extfvk, value2); - insert_into_cache(&db_cache, &cb2); - - // Scan the cache again - scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); - - // Account balance should reflect both received notes - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value + value2); - } - - #[test] - fn scan_cached_blocks_finds_change_notes() { - let cache_file = NamedTempFile::new().unwrap(); - let db_cache = CacheConnection(Connection::open(cache_file.path()).unwrap()); - init_cache_database(&db_cache).unwrap(); - - let data_file = NamedTempFile::new().unwrap(); - let db_data = DataConnection(Connection::open(data_file.path()).unwrap()); - init_data_database(&db_data).unwrap(); - - // Add an account to the wallet - let extsk = ExtendedSpendingKey::master(&[]); - let extfvk = ExtendedFullViewingKey::from(&extsk); - init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap(); - - // Account balance should be zero - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), Amount::zero()); - - // Create a fake CompactBlock sending value to the address - let value = Amount::from_u64(5).unwrap(); - let (cb, nf) = fake_compact_block( - sapling_activation_height(), - BlockHash([0; 32]), - extfvk.clone(), - value, - ); - insert_into_cache(&db_cache, &cb); - - // Scan the cache - scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); - - // Account balance should reflect the received note - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value); - - // Create a second fake CompactBlock spending value from the address - let extsk2 = ExtendedSpendingKey::master(&[0]); - let to2 = extsk2.default_address().unwrap().1; - let value2 = Amount::from_u64(2).unwrap(); - insert_into_cache( - &db_cache, - &fake_compact_block_spending( - sapling_activation_height() + 1, - cb.hash(), - (nf, value), - extfvk, - to2, - value2, - ), - ); - - // Scan the cache again - scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap(); - - // Account balance should equal the change - assert_eq!(get_balance(&db_data, AccountId(0)).unwrap(), value - value2); - } -} diff --git a/zcash_client_sqlite/src/transact.rs b/zcash_client_sqlite/src/transact.rs index c0d67c221..a25708ad8 100644 --- a/zcash_client_sqlite/src/transact.rs +++ b/zcash_client_sqlite/src/transact.rs @@ -390,8 +390,8 @@ mod tests { use crate::{ init::{init_accounts_table, init_blocks_table, init_cache_database, init_data_database}, - query::{get_balance, get_verified_balance}, tests::{self, fake_compact_block, insert_into_cache, sapling_activation_height}, + wallet::{get_balance, get_verified_balance}, AccountId, CacheConnection, DataConnection, }; diff --git a/zcash_client_sqlite/src/query.rs b/zcash_client_sqlite/src/wallet.rs similarity index 77% rename from zcash_client_sqlite/src/query.rs rename to zcash_client_sqlite/src/wallet.rs index 28389b9ef..f46b8a0c1 100644 --- a/zcash_client_sqlite/src/query.rs +++ b/zcash_client_sqlite/src/wallet.rs @@ -3,12 +3,13 @@ use rusqlite::{OptionalExtension, NO_PARAMS}; use zcash_primitives::{ - consensus::{self, BlockHeight}, + block::BlockHash, + consensus::{self, BlockHeight, NetworkUpgrade}, merkle_tree::{CommitmentTree, IncrementalWitness}, note_encryption::Memo, primitives::PaymentAddress, sapling::Node, - transaction::components::Amount, + transaction::{components::Amount, TxId}, zip32::ExtendedFullViewingKey, }; @@ -212,6 +213,110 @@ pub fn get_sent_memo_as_utf8( } } +pub fn block_height_extrema( + conn: &DataConnection, +) -> Result, rusqlite::Error> { + conn.0 + .query_row( + "SELECT MIN(height), MAX(height) FROM blocks", + NO_PARAMS, + |row| { + let min_height: u32 = row.get(0)?; + let max_height: u32 = row.get(1)?; + Ok(Some(( + BlockHeight::from(min_height), + BlockHeight::from(max_height), + ))) + }, + ) + //.optional() doesn't work here because a failed aggregate function + //produces a runtime error, not an empty set of rows. + .or(Ok(None)) +} + +pub fn get_tx_height( + conn: &DataConnection, + txid: TxId, +) -> Result, rusqlite::Error> { + conn.0 + .query_row( + "SELECT block FROM transactions WHERE txid = ?", + &[txid.0.to_vec()], + |row| row.get(0).map(u32::into), + ) + .optional() +} + +pub fn get_block_hash( + conn: &DataConnection, + block_height: BlockHeight, +) -> Result, rusqlite::Error> { + conn.0 + .query_row( + "SELECT hash FROM blocks WHERE height = ?", + &[u32::from(block_height)], + |row| { + let row_data = row.get::<_, Vec<_>>(0)?; + Ok(BlockHash::from_slice(&row_data)) + }, + ) + .optional() +} + +/// Rewinds the database to the given height. +/// +/// If the requested height is greater than or equal to the height of the last scanned +/// block, this function does nothing. +pub fn rewind_to_height( + conn: &DataConnection, + parameters: &P, + block_height: BlockHeight, +) -> Result<(), SqliteClientError> { + let sapling_activation_height = parameters + .activation_height(NetworkUpgrade::Sapling) + .ok_or(SqliteClientError(Error::SaplingNotActive))?; + + // Recall where we synced up to previously. + // If we have never synced, use Sapling activation height. + let last_scanned_height = + conn.0 + .query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| { + row.get(0) + .map(u32::into) + .or(Ok(sapling_activation_height - 1)) + })?; + + if block_height >= last_scanned_height { + // Nothing to do. + return Ok(()); + } + + // Start an SQL transaction for rewinding. + conn.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?; + + // Decrement witnesses. + conn.0.execute( + "DELETE FROM sapling_witnesses WHERE block > ?", + &[u32::from(block_height)], + )?; + + // Un-mine transactions. + conn.0.execute( + "UPDATE transactions SET block = NULL, tx_index = NULL WHERE block > ?", + &[u32::from(block_height)], + )?; + + // Now that they aren't depended on, delete scanned blocks. + conn.0.execute( + "DELETE FROM blocks WHERE height > ?", + &[u32::from(block_height)], + )?; + + // Commit the SQL transaction, rewinding atomically. + conn.0.execute("COMMIT", NO_PARAMS)?; + + Ok(()) +} pub fn get_extended_full_viewing_keys( data: &DataConnection, params: &P,