Move related functions into the same modules.
This commit is contained in:
parent
eab2951c99
commit
a181203179
|
@ -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<Option<(BlockHeight, BlockHeight)>, 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<F>(
|
||||
cache: &CacheConnection,
|
||||
from_height: BlockHeight,
|
||||
limit: Option<u32>,
|
||||
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<Option<BlockHeight>, 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<Option<BlockHash>, 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<P: consensus::Parameters>(
|
||||
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::<rusqlite::Error, NoteId>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<P: consensus::Parameters>(
|
||||
|
@ -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};
|
||||
|
||||
|
|
|
@ -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<Option<(BlockHeight, BlockHeight)>, 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<Option<BlockHash>, 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<Option<BlockHeight>, 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<P: consensus::Parameters>(
|
||||
|
@ -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<P: consensus::Parameters>(
|
||||
|
@ -127,7 +126,7 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
params: &P,
|
||||
account: AccountId,
|
||||
) -> Result<Option<PaymentAddress>, Self::Error> {
|
||||
query::get_address(self, params, account)
|
||||
wallet::get_address(self, params, account)
|
||||
}
|
||||
|
||||
fn get_account_extfvks<P: consensus::Parameters>(
|
||||
|
@ -150,47 +149,47 @@ impl<'a> DBOps for &'a DataConnection {
|
|||
}
|
||||
|
||||
fn get_balance(&self, account: AccountId) -> Result<Amount, Self::Error> {
|
||||
query::get_balance(self, account)
|
||||
wallet::get_balance(self, account)
|
||||
}
|
||||
|
||||
fn get_verified_balance(&self, account: AccountId) -> Result<Amount, Self::Error> {
|
||||
query::get_verified_balance(self, account)
|
||||
wallet::get_verified_balance(self, account)
|
||||
}
|
||||
|
||||
fn get_received_memo_as_utf8(
|
||||
&self,
|
||||
id_note: Self::NoteRef,
|
||||
) -> Result<Option<String>, 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<Option<String>, 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<P: consensus::Parameters>(
|
||||
&self,
|
||||
params: &P,
|
||||
) -> Result<Vec<ExtendedFullViewingKey>, 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<Option<CommitmentTree<Node>>, Self::Error> {
|
||||
query::get_commitment_tree(self, block_height)
|
||||
wallet::get_commitment_tree(self, block_height)
|
||||
}
|
||||
|
||||
fn get_witnesses(
|
||||
&self,
|
||||
block_height: BlockHeight,
|
||||
) -> Result<Vec<(Self::NoteRef, IncrementalWitness<Node>)>, Self::Error> {
|
||||
query::get_witnesses(self, block_height)
|
||||
wallet::get_witnesses(self, block_height)
|
||||
}
|
||||
|
||||
fn get_nullifiers(&self) -> Result<Vec<(Vec<u8>, AccountId)>, Self::Error> {
|
||||
query::get_nullifiers(self)
|
||||
wallet::get_nullifiers(self)
|
||||
}
|
||||
|
||||
fn get_update_ops(&self) -> Result<Self::UpdateOps, Self::Error> {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<u8>,
|
||||
}
|
||||
|
||||
pub fn with_cached_blocks<F>(
|
||||
cache: &CacheConnection,
|
||||
from_height: BlockHeight,
|
||||
limit: Option<u32>,
|
||||
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::<rusqlite::Error, NoteId>(
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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<Option<(BlockHeight, BlockHeight)>, 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<Option<BlockHeight>, 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<Option<BlockHash>, 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<P: consensus::Parameters>(
|
||||
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<P: consensus::Parameters>(
|
||||
data: &DataConnection,
|
||||
params: &P,
|
Loading…
Reference in New Issue