Implement chain validation & fix doctests.
This commit is contained in:
parent
a437df191e
commit
9874abfd6c
|
@ -39,15 +39,13 @@ pub fn validate_combined_chain<
|
|||
|
||||
// Recall where we synced up to previously.
|
||||
// If we have never synced, use Sapling activation height to select all cached CompactBlocks.
|
||||
let data_scan_max_height = data
|
||||
.block_height_extrema()?
|
||||
.map(|(_, max)| max)
|
||||
.unwrap_or(sapling_activation_height - 1);
|
||||
let data_max_height = data.block_height_extrema()?.map(|(_, max)| max);
|
||||
|
||||
// The cache will contain blocks above the maximum height of data in the database;
|
||||
// validate from that maximum height up to the chain tip, returning the
|
||||
// hash of the block at data_scan_max_height
|
||||
let cached_hash_opt = cache.validate_chain(data_scan_max_height, |top_block, next_block| {
|
||||
// hash of the block at data_max_height
|
||||
let from_height = data_max_height.unwrap_or(sapling_activation_height - 1);
|
||||
let cached_hash_opt = cache.validate_chain(from_height, |top_block, next_block| {
|
||||
if next_block.height() != top_block.height() - 1 {
|
||||
Err(ChainInvalid::block_height_mismatch(
|
||||
top_block.height() - 1,
|
||||
|
@ -60,22 +58,21 @@ pub fn validate_combined_chain<
|
|||
}
|
||||
})?;
|
||||
|
||||
match (cached_hash_opt, data.get_block_hash(data_scan_max_height)?) {
|
||||
(Some(cached_hash), Some(data_scan_max_hash)) =>
|
||||
// Cached blocks must hash-chain to the last scanned block.
|
||||
{
|
||||
if cached_hash == data_scan_max_hash {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ChainInvalid::prev_hash_mismatch::<E>(data_scan_max_height))
|
||||
match (cached_hash_opt, data_max_height) {
|
||||
(Some(cached_hash), Some(h)) => match data.get_block_hash(h)? {
|
||||
Some(data_scan_max_hash) => {
|
||||
if cached_hash == data_scan_max_hash {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ChainInvalid::prev_hash_mismatch::<E>(h))
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(_), None) => Err(Error::CorruptedData(
|
||||
"No block hash available at last scanned height.",
|
||||
)),
|
||||
(None, _) =>
|
||||
// No cached blocks are present, this is fine.
|
||||
{
|
||||
None => Err(Error::CorruptedData(
|
||||
"No block hash available for block at maximum chain height.",
|
||||
)),
|
||||
},
|
||||
_ => {
|
||||
// No cached blocks are present, or the max data height is absent, this is fine.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,20 +3,30 @@
|
|||
//! # Examples
|
||||
//!
|
||||
//! ```
|
||||
//! use rusqlite::Connection;
|
||||
//! use tempfile::NamedTempFile;
|
||||
//! use zcash_primitives::{
|
||||
//! consensus::{BlockHeight, Network, Parameters}
|
||||
//! };
|
||||
//!
|
||||
//! use zcash_client_backend::{
|
||||
//! data_api::{
|
||||
//! chain::validate_combined_chain,
|
||||
//! error::Error,
|
||||
//! }
|
||||
//! };
|
||||
//!
|
||||
//! use zcash_client_sqlite::{
|
||||
//! chain::{rewind_to_height, validate_combined_chain},
|
||||
//! error::ErrorKind,
|
||||
//! DataConnection,
|
||||
//! CacheConnection,
|
||||
//! chain::{rewind_to_height},
|
||||
//! scan::scan_cached_blocks,
|
||||
//! };
|
||||
//!
|
||||
//! let network = Network::TestNetwork;
|
||||
//! let db_cache = "/path/to/cache.db";
|
||||
//! let db_data = "/path/to/data.db";
|
||||
//! let cache_file = NamedTempFile::new().unwrap();
|
||||
//! let db_cache = CacheConnection::for_path(cache_file).unwrap();
|
||||
//! let data_file = NamedTempFile::new().unwrap();
|
||||
//! let db_data = DataConnection::for_path(data_file).unwrap();
|
||||
//!
|
||||
//! // 1) Download new CompactBlocks into db_cache.
|
||||
//!
|
||||
|
@ -24,18 +34,18 @@
|
|||
//! //
|
||||
//! // Given that we assume the server always gives us correct-at-the-time blocks, any
|
||||
//! // errors are in the blocks we have previously cached or scanned.
|
||||
//! if let Err(e) = validate_combined_chain(network, &db_cache, &db_data) {
|
||||
//! match e.kind() {
|
||||
//! ErrorKind::InvalidChain(upper_bound, _) => {
|
||||
//! if let Err(e) = validate_combined_chain(&network, &db_cache, &db_data) {
|
||||
//! match e {
|
||||
//! Error::InvalidChain(upper_bound, _) => {
|
||||
//! // a) Pick a height to rewind to.
|
||||
//! //
|
||||
//! // This might be informed by some external chain reorg information, or
|
||||
//! // heuristics such as the platform, available bandwidth, size of recent
|
||||
//! // CompactBlocks, etc.
|
||||
//! let rewind_height = *upper_bound - 10;
|
||||
//! let rewind_height = upper_bound - 10;
|
||||
//!
|
||||
//! // b) Rewind scanned block information.
|
||||
//! rewind_to_height(network, &db_data, rewind_height);
|
||||
//! rewind_to_height(&db_data, &network, rewind_height);
|
||||
//!
|
||||
//! // c) Delete cached blocks from rewind_height onwards.
|
||||
//! //
|
||||
|
@ -60,212 +70,201 @@
|
|||
//! // next time this codepath is executed after new blocks are received).
|
||||
//! scan_cached_blocks(&network, &db_cache, &db_data, None);
|
||||
//! ```
|
||||
|
||||
use protobuf::parse_from_bytes;
|
||||
use rusqlite::{Connection, NO_PARAMS};
|
||||
use std::path::Path;
|
||||
use rusqlite::{OptionalExtension, NO_PARAMS};
|
||||
|
||||
use zcash_primitives::consensus::{self, BlockHeight, NetworkUpgrade};
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||
};
|
||||
|
||||
use zcash_client_backend::proto::compact_formats::CompactBlock;
|
||||
use zcash_client_backend::{
|
||||
data_api::error::{ChainInvalid, Error},
|
||||
proto::compact_formats::CompactBlock,
|
||||
};
|
||||
|
||||
use crate::error::{Error, ErrorKind};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ChainInvalidCause {
|
||||
PrevHashMismatch,
|
||||
/// (expected_height, actual_height)
|
||||
HeightMismatch(BlockHeight, BlockHeight),
|
||||
}
|
||||
use crate::{error::SqliteClientError, CacheConnection, DataConnection};
|
||||
|
||||
struct CompactBlockRow {
|
||||
height: BlockHeight,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Checks that the scanned blocks in the data database, when combined with the recent
|
||||
/// `CompactBlock`s in the cache database, form a valid chain.
|
||||
///
|
||||
/// This function is built on the core assumption that the information provided in the
|
||||
/// cache database is more likely to be accurate than the previously-scanned information.
|
||||
/// This follows from the design (and trust) assumption that the `lightwalletd` server
|
||||
/// provides accurate block information as of the time it was requested.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Ok(())` if the combined chain is valid.
|
||||
/// - `Err(ErrorKind::InvalidChain(upper_bound, cause))` if the combined chain is invalid.
|
||||
/// `upper_bound` is the height of the highest invalid block (on the assumption that the
|
||||
/// highest block in the cache database is correct).
|
||||
/// - `Err(e)` if there was an error during validation unrelated to chain validity.
|
||||
///
|
||||
/// This function does not mutate either of the databases.
|
||||
pub fn validate_combined_chain<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRef<Path>>(
|
||||
parameters: Params,
|
||||
db_cache: P,
|
||||
db_data: Q,
|
||||
) -> Result<(), Error> {
|
||||
let cache = Connection::open(db_cache)?;
|
||||
let data = Connection::open(db_data)?;
|
||||
let sapling_activation_height = parameters
|
||||
.activation_height(NetworkUpgrade::Sapling)
|
||||
.ok_or(Error(ErrorKind::SaplingNotActive))?;
|
||||
pub fn validate_chain<F>(
|
||||
conn: &CacheConnection,
|
||||
from_height: BlockHeight,
|
||||
validate: F,
|
||||
) -> Result<Option<BlockHash>, SqliteClientError>
|
||||
where
|
||||
F: Fn(&CompactBlock, &CompactBlock) -> Result<(), Error<rusqlite::Error>>,
|
||||
{
|
||||
let mut stmt_blocks = conn
|
||||
.0
|
||||
.prepare("SELECT height, data FROM compactblocks WHERE height >= ? ORDER BY height DESC")?;
|
||||
|
||||
// Recall where we synced up to previously.
|
||||
// If we have never synced, use Sapling activation height to select all cached CompactBlocks.
|
||||
let (have_scanned, last_scanned_height) =
|
||||
data.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
|
||||
row.get(0)
|
||||
.map(|h: u32| (true, h.into()))
|
||||
.or(Ok((false, sapling_activation_height - 1)))
|
||||
})?;
|
||||
|
||||
// Fetch the CompactBlocks we need to validate
|
||||
let mut stmt_blocks = cache
|
||||
.prepare("SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height DESC")?;
|
||||
let mut rows = stmt_blocks.query_map(&[u32::from(last_scanned_height)], |row| {
|
||||
Ok(CompactBlockRow {
|
||||
height: row.get(0).map(u32::into)?,
|
||||
data: row.get(1)?,
|
||||
})
|
||||
let block_rows = stmt_blocks.query_map(&[u32::from(from_height)], |row| {
|
||||
let height: BlockHeight = row.get(0).map(u32::into)?;
|
||||
let data = row.get::<_, Vec<_>>(1)?;
|
||||
Ok(CompactBlockRow { height, data })
|
||||
})?;
|
||||
|
||||
// Take the highest cached block as accurate.
|
||||
let (mut last_height, mut last_prev_hash) = {
|
||||
let assumed_correct = match rows.next() {
|
||||
Some(row) => row?,
|
||||
None => {
|
||||
// No cached blocks, and we've already validated the blocks we've scanned,
|
||||
// so there's nothing to validate.
|
||||
// TODO: Maybe we still want to check if there are cached blocks that are at heights we previously scanned? Check scanning flow again.
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let block: CompactBlock = parse_from_bytes(&assumed_correct.data)?;
|
||||
(block.height(), block.prev_hash())
|
||||
let mut blocks = block_rows.map(|cbr_result| {
|
||||
let cbr = cbr_result.map_err(Error::Database)?;
|
||||
let block: CompactBlock = parse_from_bytes(&cbr.data).map_err(Error::from)?;
|
||||
|
||||
if block.height() == cbr.height {
|
||||
Ok(block)
|
||||
} else {
|
||||
Err(ChainInvalid::block_height_mismatch(
|
||||
cbr.height,
|
||||
block.height(),
|
||||
))
|
||||
}
|
||||
});
|
||||
|
||||
let mut current_block: CompactBlock = match blocks.next() {
|
||||
Some(Ok(block)) => block,
|
||||
Some(Err(error)) => {
|
||||
return Err(SqliteClientError(error));
|
||||
}
|
||||
None => {
|
||||
// No cached blocks, and we've already validated the blocks we've scanned,
|
||||
// so there's nothing to validate.
|
||||
// TODO: Maybe we still want to check if there are cached blocks that are
|
||||
// at heights we previously scanned? Check scanning flow again.
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
for row in rows {
|
||||
let row = row?;
|
||||
|
||||
// Scanned blocks MUST be height-sequential.
|
||||
if row.height != (last_height - 1) {
|
||||
return Err(Error(ErrorKind::InvalidChain(
|
||||
last_height - 1,
|
||||
ChainInvalidCause::HeightMismatch(last_height - 1, row.height),
|
||||
)));
|
||||
}
|
||||
last_height = row.height;
|
||||
|
||||
let block: CompactBlock = parse_from_bytes(&row.data)?;
|
||||
|
||||
// Cached blocks MUST be hash-chained.
|
||||
if block.hash() != last_prev_hash {
|
||||
return Err(Error(ErrorKind::InvalidChain(
|
||||
last_height,
|
||||
ChainInvalidCause::PrevHashMismatch,
|
||||
)));
|
||||
}
|
||||
last_prev_hash = block.prev_hash();
|
||||
for block_result in blocks {
|
||||
let block = block_result?;
|
||||
validate(¤t_block, &block)?;
|
||||
current_block = block;
|
||||
}
|
||||
|
||||
if have_scanned {
|
||||
// Cached blocks MUST hash-chain to the last scanned block.
|
||||
let last_scanned_hash = data.query_row(
|
||||
"SELECT hash FROM blocks WHERE height = ?",
|
||||
&[u32::from(last_scanned_height)],
|
||||
|row| row.get::<_, Vec<_>>(0),
|
||||
)?;
|
||||
if &last_scanned_hash[..] != &last_prev_hash.0[..] {
|
||||
return Err(Error(ErrorKind::InvalidChain(
|
||||
last_scanned_height,
|
||||
ChainInvalidCause::PrevHashMismatch,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// All good!
|
||||
Ok(())
|
||||
Ok(Some(current_block.hash()))
|
||||
}
|
||||
|
||||
/// Rewinds the data database to the given height.
|
||||
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())))
|
||||
},
|
||||
)
|
||||
// cannot use .optional() here because the result of a failed group
|
||||
// operation is an error, not an empty row.
|
||||
.or(Ok(None))
|
||||
}
|
||||
|
||||
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<Params: consensus::Parameters, P: AsRef<Path>>(
|
||||
parameters: Params,
|
||||
db_data: P,
|
||||
height: BlockHeight,
|
||||
) -> Result<(), Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
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(Error(ErrorKind::SaplingNotActive))?;
|
||||
.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 =
|
||||
data.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
|
||||
row.get(0)
|
||||
.map(u32::into)
|
||||
.or(Ok(sapling_activation_height - 1))
|
||||
})?;
|
||||
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 height >= last_scanned_height {
|
||||
if block_height >= last_scanned_height {
|
||||
// Nothing to do.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Start an SQL transaction for rewinding.
|
||||
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
conn.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
|
||||
// Decrement witnesses.
|
||||
data.execute(
|
||||
conn.0.execute(
|
||||
"DELETE FROM sapling_witnesses WHERE block > ?",
|
||||
&[u32::from(height)],
|
||||
&[u32::from(block_height)],
|
||||
)?;
|
||||
|
||||
// Un-mine transactions.
|
||||
data.execute(
|
||||
conn.0.execute(
|
||||
"UPDATE transactions SET block = NULL, tx_index = NULL WHERE block > ?",
|
||||
&[u32::from(height)],
|
||||
&[u32::from(block_height)],
|
||||
)?;
|
||||
|
||||
// Now that they aren't depended on, delete scanned blocks.
|
||||
data.execute("DELETE FROM blocks WHERE height > ?", &[u32::from(height)])?;
|
||||
conn.0.execute(
|
||||
"DELETE FROM blocks WHERE height > ?",
|
||||
&[u32::from(block_height)],
|
||||
)?;
|
||||
|
||||
// Commit the SQL transaction, rewinding atomically.
|
||||
data.execute("COMMIT", NO_PARAMS)?;
|
||||
conn.0.execute("COMMIT", NO_PARAMS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rusqlite::Connection;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
transaction::components::Amount,
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use super::{rewind_to_height, validate_combined_chain};
|
||||
use zcash_client_backend::data_api::{chain::validate_combined_chain, error::Error};
|
||||
|
||||
use crate::{
|
||||
error::ErrorKind,
|
||||
init::{init_accounts_table, init_cache_database, init_data_database},
|
||||
query::get_balance,
|
||||
scan::scan_cached_blocks,
|
||||
tests::{self, fake_compact_block, insert_into_cache, sapling_activation_height},
|
||||
CacheConnection, DataConnection,
|
||||
};
|
||||
|
||||
use super::rewind_to_height;
|
||||
|
||||
#[test]
|
||||
fn valid_chain_states() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
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 = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -274,7 +273,7 @@ mod tests {
|
|||
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Empty chain should be valid
|
||||
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
|
||||
validate_combined_chain(&tests::network(), &db_cache, &db_data).unwrap();
|
||||
|
||||
// Create a fake CompactBlock sending value to the address
|
||||
let (cb, _) = fake_compact_block(
|
||||
|
@ -283,16 +282,16 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
Amount::from_u64(5).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
|
||||
// Cache-only chain should be valid
|
||||
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
|
||||
validate_combined_chain(&tests::network(), &db_cache, &db_data).unwrap();
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
|
||||
validate_combined_chain(&tests::network(), &db_cache, &db_data).unwrap();
|
||||
|
||||
// Create a second fake CompactBlock sending more value to the address
|
||||
let (cb2, _) = fake_compact_block(
|
||||
|
@ -301,26 +300,26 @@ mod tests {
|
|||
extfvk,
|
||||
Amount::from_u64(7).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb2);
|
||||
insert_into_cache(&db_cache, &cb2);
|
||||
|
||||
// Data+cache chain should be valid
|
||||
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
|
||||
validate_combined_chain(&tests::network(), &db_cache, &db_data).unwrap();
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
|
||||
validate_combined_chain(&tests::network(), &db_cache, &db_data).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_chain_cache_disconnected() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
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 = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -341,14 +340,14 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
Amount::from_u64(7).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(db_cache, &cb2);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
insert_into_cache(&db_cache, &cb2);
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
|
||||
validate_combined_chain(&tests::network(), &db_cache, &db_data).unwrap();
|
||||
|
||||
// Create more fake CompactBlocks that don't connect to the scanned ones
|
||||
let (cb3, _) = fake_compact_block(
|
||||
|
@ -363,17 +362,14 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
Amount::from_u64(3).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb3);
|
||||
insert_into_cache(db_cache, &cb4);
|
||||
insert_into_cache(&db_cache, &cb3);
|
||||
insert_into_cache(&db_cache, &cb4);
|
||||
|
||||
// Data+cache chain should be invalid at the data/cache boundary
|
||||
match validate_combined_chain(tests::network(), db_cache, db_data) {
|
||||
Err(e) => match e.kind() {
|
||||
ErrorKind::InvalidChain(upper_bound, _) => {
|
||||
assert_eq!(*upper_bound, sapling_activation_height() + 1)
|
||||
}
|
||||
_ => panic!(),
|
||||
},
|
||||
match validate_combined_chain(&tests::network(), &db_cache, &db_data) {
|
||||
Err(Error::InvalidChain(upper_bound, _)) => {
|
||||
assert_eq!(upper_bound, sapling_activation_height() + 1)
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
@ -381,11 +377,11 @@ mod tests {
|
|||
#[test]
|
||||
fn invalid_chain_cache_reorg() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
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 = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -406,14 +402,14 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
Amount::from_u64(7).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(db_cache, &cb2);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
insert_into_cache(&db_cache, &cb2);
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Data-only chain should be valid
|
||||
validate_combined_chain(tests::network(), db_cache, db_data).unwrap();
|
||||
validate_combined_chain(&tests::network(), &db_cache, &db_data).unwrap();
|
||||
|
||||
// Create more fake CompactBlocks that contain a reorg
|
||||
let (cb3, _) = fake_compact_block(
|
||||
|
@ -428,17 +424,14 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
Amount::from_u64(3).unwrap(),
|
||||
);
|
||||
insert_into_cache(db_cache, &cb3);
|
||||
insert_into_cache(db_cache, &cb4);
|
||||
insert_into_cache(&db_cache, &cb3);
|
||||
insert_into_cache(&db_cache, &cb4);
|
||||
|
||||
// Data+cache chain should be invalid inside the cache
|
||||
match validate_combined_chain(tests::network(), db_cache, db_data) {
|
||||
Err(e) => match e.kind() {
|
||||
ErrorKind::InvalidChain(upper_bound, _) => {
|
||||
assert_eq!(*upper_bound, sapling_activation_height() + 2)
|
||||
}
|
||||
_ => panic!(),
|
||||
},
|
||||
match validate_combined_chain(&tests::network(), &db_cache, &db_data) {
|
||||
Err(Error::InvalidChain(upper_bound, _)) => {
|
||||
assert_eq!(upper_bound, sapling_activation_height() + 2)
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
|
@ -446,11 +439,11 @@ mod tests {
|
|||
#[test]
|
||||
fn data_db_rewinding() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
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 = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -459,7 +452,7 @@ mod tests {
|
|||
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), Amount::zero());
|
||||
|
||||
// Create fake CompactBlocks sending value to the address
|
||||
let value = Amount::from_u64(5).unwrap();
|
||||
|
@ -470,33 +463,34 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
|
||||
let (cb2, _) =
|
||||
fake_compact_block(sapling_activation_height() + 1, cb.hash(), extfvk, value2);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(db_cache, &cb2);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
insert_into_cache(&db_cache, &cb2);
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect both received notes
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), value + value2);
|
||||
|
||||
// "Rewind" to height of last scanned block
|
||||
rewind_to_height(tests::network(), db_data, sapling_activation_height() + 1).unwrap();
|
||||
rewind_to_height(&db_data, &tests::network(), sapling_activation_height() + 1).unwrap();
|
||||
|
||||
// Account balance should be unaltered
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), value + value2);
|
||||
|
||||
// Rewind so that one block is dropped
|
||||
rewind_to_height(tests::network(), db_data, sapling_activation_height()).unwrap();
|
||||
rewind_to_height(&db_data, &tests::network(), sapling_activation_height()).unwrap();
|
||||
|
||||
// Account balance should only contain the first received note
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), value);
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Account balance should again reflect both received notes
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), value + value2);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,135 +1,56 @@
|
|||
use std::error;
|
||||
use std::fmt;
|
||||
use zcash_primitives::{
|
||||
consensus::BlockHeight,
|
||||
sapling::Node,
|
||||
transaction::{builder, TxId},
|
||||
};
|
||||
|
||||
use zcash_primitives::transaction::builder;
|
||||
|
||||
use zcash_client_backend::data_api::error::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ErrorKind {
|
||||
CorruptedData(&'static str),
|
||||
IncorrectHRPExtFVK,
|
||||
InsufficientBalance(u64, u64),
|
||||
InvalidChain(BlockHeight, crate::chain::ChainInvalidCause),
|
||||
InvalidExtSK(u32),
|
||||
InvalidHeight(BlockHeight, BlockHeight),
|
||||
InvalidMemo(std::str::Utf8Error),
|
||||
InvalidNewWitnessAnchor(usize, TxId, BlockHeight, Node),
|
||||
InvalidNote,
|
||||
InvalidWitnessAnchor(i64, BlockHeight),
|
||||
ScanRequired,
|
||||
TableNotEmpty,
|
||||
Bech32(bech32::Error),
|
||||
Base58(bs58::decode::Error),
|
||||
Builder(builder::Error),
|
||||
Database(rusqlite::Error),
|
||||
Io(std::io::Error),
|
||||
Protobuf(protobuf::ProtobufError),
|
||||
SaplingNotActive,
|
||||
}
|
||||
pub struct SqliteClientError(pub Error<rusqlite::Error>);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error(pub(crate) ErrorKind);
|
||||
|
||||
impl fmt::Display for Error {
|
||||
impl fmt::Display for SqliteClientError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self.0 {
|
||||
ErrorKind::CorruptedData(reason) => write!(f, "Data DB is corrupted: {}", reason),
|
||||
ErrorKind::IncorrectHRPExtFVK => write!(f, "Incorrect HRP for extfvk"),
|
||||
ErrorKind::InsufficientBalance(have, need) => write!(
|
||||
f,
|
||||
"Insufficient balance (have {}, need {} including fee)",
|
||||
have, need
|
||||
),
|
||||
ErrorKind::InvalidChain(upper_bound, cause) => {
|
||||
write!(f, "Invalid chain (upper bound: {}): {:?}", upper_bound, cause)
|
||||
}
|
||||
ErrorKind::InvalidExtSK(account) => {
|
||||
write!(f, "Incorrect ExtendedSpendingKey for account {}", account)
|
||||
}
|
||||
ErrorKind::InvalidHeight(expected, actual) => write!(
|
||||
f,
|
||||
"Expected height of next CompactBlock to be {}, but was {}",
|
||||
expected, actual
|
||||
),
|
||||
ErrorKind::InvalidMemo(e) => write!(f, "{}", e),
|
||||
ErrorKind::InvalidNewWitnessAnchor(output, txid, last_height, anchor) => write!(
|
||||
f,
|
||||
"New witness for output {} in tx {} has incorrect anchor after scanning block {}: {:?}",
|
||||
output, txid, last_height, anchor,
|
||||
),
|
||||
ErrorKind::InvalidNote => write!(f, "Invalid note"),
|
||||
ErrorKind::InvalidWitnessAnchor(id_note, last_height) => write!(
|
||||
f,
|
||||
"Witness for note {} has incorrect anchor after scanning block {}",
|
||||
id_note, last_height
|
||||
),
|
||||
ErrorKind::ScanRequired => write!(f, "Must scan blocks first"),
|
||||
ErrorKind::TableNotEmpty => write!(f, "Table is not empty"),
|
||||
ErrorKind::Bech32(e) => write!(f, "{}", e),
|
||||
ErrorKind::Base58(e) => write!(f, "{}", e),
|
||||
ErrorKind::Builder(e) => write!(f, "{:?}", e),
|
||||
ErrorKind::Database(e) => write!(f, "{}", e),
|
||||
ErrorKind::Io(e) => write!(f, "{}", e),
|
||||
ErrorKind::Protobuf(e) => write!(f, "{}", e),
|
||||
ErrorKind::SaplingNotActive => write!(f, "Sapling activation height not specified for network."),
|
||||
}
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
match &self.0 {
|
||||
ErrorKind::InvalidMemo(e) => Some(e),
|
||||
ErrorKind::Bech32(e) => Some(e),
|
||||
ErrorKind::Builder(e) => Some(e),
|
||||
ErrorKind::Database(e) => Some(e),
|
||||
ErrorKind::Io(e) => Some(e),
|
||||
ErrorKind::Protobuf(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
impl From<Error<rusqlite::Error>> for SqliteClientError {
|
||||
fn from(e: Error<rusqlite::Error>) -> Self {
|
||||
SqliteClientError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bech32::Error> for Error {
|
||||
impl From<bech32::Error> for SqliteClientError {
|
||||
fn from(e: bech32::Error) -> Self {
|
||||
Error(ErrorKind::Bech32(e))
|
||||
SqliteClientError(Error::Bech32(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bs58::decode::Error> for Error {
|
||||
fn from(e: bs58::decode::Error) -> Self {
|
||||
Error(ErrorKind::Base58(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<builder::Error> for Error {
|
||||
fn from(e: builder::Error) -> Self {
|
||||
Error(ErrorKind::Builder(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rusqlite::Error> for Error {
|
||||
impl From<rusqlite::Error> for SqliteClientError {
|
||||
fn from(e: rusqlite::Error) -> Self {
|
||||
Error(ErrorKind::Database(e))
|
||||
SqliteClientError(Error::Database(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
impl From<bs58::decode::Error> for SqliteClientError {
|
||||
fn from(e: bs58::decode::Error) -> Self {
|
||||
SqliteClientError(Error::Base58(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<builder::Error> for SqliteClientError {
|
||||
fn from(e: builder::Error) -> Self {
|
||||
SqliteClientError(Error::Builder(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for SqliteClientError {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Error(ErrorKind::Io(e))
|
||||
SqliteClientError(Error::Io(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<protobuf::ProtobufError> for Error {
|
||||
impl From<protobuf::ProtobufError> for SqliteClientError {
|
||||
fn from(e: protobuf::ProtobufError) -> Self {
|
||||
Error(ErrorKind::Protobuf(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn kind(&self) -> &ErrorKind {
|
||||
&self.0
|
||||
SqliteClientError(Error::Protobuf(e))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
//! Functions for initializing the various databases.
|
||||
|
||||
use rusqlite::{types::ToSql, Connection, NO_PARAMS};
|
||||
use std::path::Path;
|
||||
use rusqlite::{types::ToSql, NO_PARAMS};
|
||||
use zcash_client_backend::encoding::encode_extended_full_viewing_key;
|
||||
|
||||
use zcash_primitives::{block::BlockHash, consensus, zip32::ExtendedFullViewingKey};
|
||||
|
||||
use crate::{
|
||||
address_from_extfvk,
|
||||
error::{Error, ErrorKind},
|
||||
};
|
||||
use zcash_client_backend::data_api::error::Error;
|
||||
|
||||
use crate::{address_from_extfvk, error::SqliteClientError, CacheConnection, DataConnection};
|
||||
|
||||
/// Sets up the internal structure of the cache database.
|
||||
///
|
||||
|
@ -17,15 +15,17 @@ use crate::{
|
|||
///
|
||||
/// ```
|
||||
/// use tempfile::NamedTempFile;
|
||||
/// use zcash_client_sqlite::init::init_cache_database;
|
||||
/// use zcash_client_sqlite::{
|
||||
/// CacheConnection,
|
||||
/// init::init_cache_database,
|
||||
/// };
|
||||
///
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db_cache = data_file.path();
|
||||
/// init_cache_database(&db_cache).unwrap();
|
||||
/// let cache_file = NamedTempFile::new().unwrap();
|
||||
/// let db = CacheConnection::for_path(cache_file.path()).unwrap();
|
||||
/// init_cache_database(&db).unwrap();
|
||||
/// ```
|
||||
pub fn init_cache_database<P: AsRef<Path>>(db_cache: P) -> Result<(), Error> {
|
||||
let cache = Connection::open(db_cache)?;
|
||||
cache.execute(
|
||||
pub fn init_cache_database(db_cache: &CacheConnection) -> Result<(), rusqlite::Error> {
|
||||
db_cache.0.execute(
|
||||
"CREATE TABLE IF NOT EXISTS compactblocks (
|
||||
height INTEGER PRIMARY KEY,
|
||||
data BLOB NOT NULL
|
||||
|
@ -41,15 +41,17 @@ pub fn init_cache_database<P: AsRef<Path>>(db_cache: P) -> Result<(), Error> {
|
|||
///
|
||||
/// ```
|
||||
/// use tempfile::NamedTempFile;
|
||||
/// use zcash_client_sqlite::init::init_data_database;
|
||||
/// use zcash_client_sqlite::{
|
||||
/// DataConnection,
|
||||
/// init::init_data_database,
|
||||
/// };
|
||||
///
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db_data = data_file.path();
|
||||
/// init_data_database(&db_data).unwrap();
|
||||
/// let db = DataConnection::for_path(data_file.path()).unwrap();
|
||||
/// init_data_database(&db).unwrap();
|
||||
/// ```
|
||||
pub fn init_data_database<P: AsRef<Path>>(db_data: P) -> Result<(), Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
data.execute(
|
||||
pub fn init_data_database(db_data: &DataConnection) -> Result<(), rusqlite::Error> {
|
||||
db_data.0.execute(
|
||||
"CREATE TABLE IF NOT EXISTS accounts (
|
||||
account INTEGER PRIMARY KEY,
|
||||
extfvk TEXT NOT NULL,
|
||||
|
@ -57,7 +59,7 @@ pub fn init_data_database<P: AsRef<Path>>(db_data: P) -> Result<(), Error> {
|
|||
)",
|
||||
NO_PARAMS,
|
||||
)?;
|
||||
data.execute(
|
||||
db_data.0.execute(
|
||||
"CREATE TABLE IF NOT EXISTS blocks (
|
||||
height INTEGER PRIMARY KEY,
|
||||
hash BLOB NOT NULL,
|
||||
|
@ -66,7 +68,7 @@ pub fn init_data_database<P: AsRef<Path>>(db_data: P) -> Result<(), Error> {
|
|||
)",
|
||||
NO_PARAMS,
|
||||
)?;
|
||||
data.execute(
|
||||
db_data.0.execute(
|
||||
"CREATE TABLE IF NOT EXISTS transactions (
|
||||
id_tx INTEGER PRIMARY KEY,
|
||||
txid BLOB NOT NULL UNIQUE,
|
||||
|
@ -79,7 +81,7 @@ pub fn init_data_database<P: AsRef<Path>>(db_data: P) -> Result<(), Error> {
|
|||
)",
|
||||
NO_PARAMS,
|
||||
)?;
|
||||
data.execute(
|
||||
db_data.0.execute(
|
||||
"CREATE TABLE IF NOT EXISTS received_notes (
|
||||
id_note INTEGER PRIMARY KEY,
|
||||
tx INTEGER NOT NULL,
|
||||
|
@ -99,7 +101,7 @@ pub fn init_data_database<P: AsRef<Path>>(db_data: P) -> Result<(), Error> {
|
|||
)",
|
||||
NO_PARAMS,
|
||||
)?;
|
||||
data.execute(
|
||||
db_data.0.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sapling_witnesses (
|
||||
id_witness INTEGER PRIMARY KEY,
|
||||
note INTEGER NOT NULL,
|
||||
|
@ -111,7 +113,7 @@ pub fn init_data_database<P: AsRef<Path>>(db_data: P) -> Result<(), Error> {
|
|||
)",
|
||||
NO_PARAMS,
|
||||
)?;
|
||||
data.execute(
|
||||
db_data.0.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sent_notes (
|
||||
id_note INTEGER PRIMARY KEY,
|
||||
tx INTEGER NOT NULL,
|
||||
|
@ -140,14 +142,19 @@ pub fn init_data_database<P: AsRef<Path>>(db_data: P) -> Result<(), Error> {
|
|||
///
|
||||
/// ```
|
||||
/// use tempfile::NamedTempFile;
|
||||
/// use zcash_client_sqlite::init::{init_accounts_table, init_data_database};
|
||||
///
|
||||
/// use zcash_primitives::{
|
||||
/// consensus::Network,
|
||||
/// zip32::{ExtendedFullViewingKey, ExtendedSpendingKey}
|
||||
/// };
|
||||
///
|
||||
/// use zcash_client_sqlite::{
|
||||
/// DataConnection,
|
||||
/// init::{init_accounts_table, init_data_database}
|
||||
/// };
|
||||
///
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db_data = data_file.path();
|
||||
/// let db_data = DataConnection::for_path(data_file.path()).unwrap();
|
||||
/// init_data_database(&db_data).unwrap();
|
||||
///
|
||||
/// let extsk = ExtendedSpendingKey::master(&[]);
|
||||
|
@ -158,27 +165,25 @@ pub fn init_data_database<P: AsRef<Path>>(db_data: P) -> Result<(), Error> {
|
|||
/// [`get_address`]: crate::query::get_address
|
||||
/// [`scan_cached_blocks`]: crate::scan::scan_cached_blocks
|
||||
/// [`create_to_address`]: crate::transact::create_to_address
|
||||
pub fn init_accounts_table<D: AsRef<Path>, P: consensus::Parameters>(
|
||||
db_data: D,
|
||||
pub fn init_accounts_table<P: consensus::Parameters>(
|
||||
data: &DataConnection,
|
||||
params: &P,
|
||||
extfvks: &[ExtendedFullViewingKey],
|
||||
) -> Result<(), Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
let mut empty_check = data.prepare("SELECT * FROM accounts LIMIT 1")?;
|
||||
) -> Result<(), SqliteClientError> {
|
||||
let mut empty_check = data.0.prepare("SELECT * FROM accounts LIMIT 1")?;
|
||||
if empty_check.exists(NO_PARAMS)? {
|
||||
return Err(Error(ErrorKind::TableNotEmpty));
|
||||
return Err(SqliteClientError(Error::TableNotEmpty));
|
||||
}
|
||||
|
||||
// Insert accounts atomically
|
||||
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
data.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
for (account, extfvk) in extfvks.iter().enumerate() {
|
||||
let address = address_from_extfvk(params, extfvk);
|
||||
let extfvk = encode_extended_full_viewing_key(
|
||||
params.hrp_sapling_extended_full_viewing_key(),
|
||||
extfvk,
|
||||
);
|
||||
data.execute(
|
||||
data.0.execute(
|
||||
"INSERT INTO accounts (account, extfvk, address)
|
||||
VALUES (?, ?, ?)",
|
||||
&[
|
||||
|
@ -188,7 +193,7 @@ pub fn init_accounts_table<D: AsRef<Path>, P: consensus::Parameters>(
|
|||
],
|
||||
)?;
|
||||
}
|
||||
data.execute("COMMIT", NO_PARAMS)?;
|
||||
data.0.execute("COMMIT", NO_PARAMS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -201,8 +206,12 @@ pub fn init_accounts_table<D: AsRef<Path>, P: consensus::Parameters>(
|
|||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::init::init_blocks_table;
|
||||
/// use tempfile::NamedTempFile;
|
||||
/// use zcash_primitives::block::BlockHash;
|
||||
/// use zcash_client_sqlite::{
|
||||
/// DataConnection,
|
||||
/// init::init_blocks_table,
|
||||
/// };
|
||||
///
|
||||
/// // The block height.
|
||||
/// let height = 500_000;
|
||||
|
@ -214,23 +223,23 @@ pub fn init_accounts_table<D: AsRef<Path>, P: consensus::Parameters>(
|
|||
/// // Pre-compute and hard-code, or obtain from a service.
|
||||
/// let sapling_tree = &[];
|
||||
///
|
||||
/// init_blocks_table("/path/to/data.db", height, hash, time, sapling_tree);
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db = DataConnection::for_path(data_file.path()).unwrap();
|
||||
/// init_blocks_table(&db, height, hash, time, sapling_tree);
|
||||
/// ```
|
||||
pub fn init_blocks_table<P: AsRef<Path>>(
|
||||
db_data: P,
|
||||
pub fn init_blocks_table(
|
||||
data: &DataConnection,
|
||||
height: i32,
|
||||
hash: BlockHash,
|
||||
time: u32,
|
||||
sapling_tree: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
let mut empty_check = data.prepare("SELECT * FROM blocks LIMIT 1")?;
|
||||
) -> Result<(), SqliteClientError> {
|
||||
let mut empty_check = data.0.prepare("SELECT * FROM blocks LIMIT 1")?;
|
||||
if empty_check.exists(NO_PARAMS)? {
|
||||
return Err(Error(ErrorKind::TableNotEmpty));
|
||||
return Err(SqliteClientError(Error::TableNotEmpty));
|
||||
}
|
||||
|
||||
data.execute(
|
||||
data.0.execute(
|
||||
"INSERT INTO blocks (height, hash, time, sapling_tree)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
&[
|
||||
|
@ -246,21 +255,25 @@ pub fn init_blocks_table<P: AsRef<Path>>(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rusqlite::Connection;
|
||||
use tempfile::NamedTempFile;
|
||||
use zcash_client_backend::encoding::decode_payment_address;
|
||||
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::Parameters,
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use zcash_client_backend::encoding::decode_payment_address;
|
||||
|
||||
use crate::{query::get_address, tests, DataConnection};
|
||||
|
||||
use super::{init_accounts_table, init_blocks_table, init_data_database};
|
||||
use crate::{query::get_address, tests};
|
||||
|
||||
#[test]
|
||||
fn init_accounts_table_only_works_once() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// We can call the function as many times as we want with no data
|
||||
|
@ -281,7 +294,7 @@ mod tests {
|
|||
#[test]
|
||||
fn init_blocks_table_only_works_once() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// First call with data should initialise the blocks table
|
||||
|
@ -294,7 +307,7 @@ mod tests {
|
|||
#[test]
|
||||
fn init_accounts_table_stores_correct_address() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
|
|
@ -26,13 +26,21 @@
|
|||
|
||||
use rusqlite::{Connection, NO_PARAMS};
|
||||
use std::cmp;
|
||||
use std::path::Path;
|
||||
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
consensus::{self, BlockHeight},
|
||||
zip32::ExtendedFullViewingKey,
|
||||
};
|
||||
|
||||
use zcash_client_backend::encoding::encode_payment_address;
|
||||
use zcash_client_backend::{
|
||||
data_api::{chain::ANCHOR_OFFSET, error::Error, CacheOps, DBOps},
|
||||
encoding::encode_payment_address,
|
||||
proto::compact_formats::CompactBlock,
|
||||
};
|
||||
|
||||
use crate::error::SqliteClientError;
|
||||
|
||||
pub mod chain;
|
||||
pub mod error;
|
||||
|
@ -41,7 +49,58 @@ pub mod query;
|
|||
pub mod scan;
|
||||
pub mod transact;
|
||||
|
||||
const ANCHOR_OFFSET: u32 = 10;
|
||||
pub struct Account(u32);
|
||||
|
||||
pub struct DataConnection(Connection);
|
||||
|
||||
impl DataConnection {
|
||||
pub fn for_path<P: AsRef<Path>>(path: P) -> Result<Self, rusqlite::Error> {
|
||||
Connection::open(path).map(DataConnection)
|
||||
}
|
||||
}
|
||||
|
||||
impl DBOps for DataConnection {
|
||||
type Error = Error<rusqlite::Error>;
|
||||
|
||||
fn block_height_extrema(&self) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
|
||||
chain::block_height_extrema(self).map_err(Error::Database)
|
||||
}
|
||||
|
||||
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
|
||||
chain::get_block_hash(self, block_height).map_err(Error::Database)
|
||||
}
|
||||
|
||||
fn rewind_to_height<P: consensus::Parameters>(
|
||||
&self,
|
||||
parameters: &P,
|
||||
block_height: BlockHeight,
|
||||
) -> Result<(), Self::Error> {
|
||||
chain::rewind_to_height(self, parameters, block_height).map_err(|e| e.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CacheConnection(Connection);
|
||||
|
||||
impl CacheConnection {
|
||||
pub fn for_path<P: AsRef<Path>>(path: P) -> Result<Self, rusqlite::Error> {
|
||||
Connection::open(path).map(CacheConnection)
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheOps for CacheConnection {
|
||||
type Error = Error<rusqlite::Error>;
|
||||
|
||||
fn validate_chain<F>(
|
||||
&self,
|
||||
from_height: BlockHeight,
|
||||
validate: F,
|
||||
) -> Result<Option<BlockHash>, Self::Error>
|
||||
where
|
||||
F: Fn(&CompactBlock, &CompactBlock) -> Result<(), Self::Error>,
|
||||
{
|
||||
chain::validate_chain(self, from_height, validate).map_err(|s| s.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn address_from_extfvk<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
|
@ -54,16 +113,16 @@ fn address_from_extfvk<P: consensus::Parameters>(
|
|||
/// Determines the target height for a transaction, and the height from which to
|
||||
/// select anchors, based on the current synchronised block chain.
|
||||
fn get_target_and_anchor_heights(
|
||||
data: &Connection,
|
||||
) -> Result<(BlockHeight, BlockHeight), error::Error> {
|
||||
data.query_row_and_then(
|
||||
data: &DataConnection,
|
||||
) -> Result<(BlockHeight, BlockHeight), SqliteClientError> {
|
||||
data.0.query_row_and_then(
|
||||
"SELECT MIN(height), MAX(height) FROM blocks",
|
||||
NO_PARAMS,
|
||||
|row| match (row.get::<_, u32>(0), row.get::<_, u32>(1)) {
|
||||
// If there are no blocks, the query returns NULL.
|
||||
(Err(rusqlite::Error::InvalidColumnType(_, _, _)), _)
|
||||
| (_, Err(rusqlite::Error::InvalidColumnType(_, _, _))) => {
|
||||
Err(error::Error(error::ErrorKind::ScanRequired))
|
||||
Err(Error::ScanRequired.into())
|
||||
}
|
||||
(Err(e), _) | (_, Err(e)) => Err(e.into()),
|
||||
(Ok(min_height), Ok(max_height)) => {
|
||||
|
@ -89,8 +148,7 @@ mod tests {
|
|||
use group::GroupEncoding;
|
||||
use protobuf::Message;
|
||||
use rand_core::{OsRng, RngCore};
|
||||
use rusqlite::{types::ToSql, Connection};
|
||||
use std::path::Path;
|
||||
use rusqlite::types::ToSql;
|
||||
|
||||
use zcash_client_backend::proto::compact_formats::{
|
||||
CompactBlock, CompactOutput, CompactSpend, CompactTx,
|
||||
|
@ -106,6 +164,8 @@ mod tests {
|
|||
zip32::ExtendedFullViewingKey,
|
||||
};
|
||||
|
||||
use super::CacheConnection;
|
||||
|
||||
#[cfg(feature = "mainnet")]
|
||||
pub(crate) fn network() -> Network {
|
||||
Network::MainNetwork
|
||||
|
@ -265,10 +325,10 @@ mod tests {
|
|||
}
|
||||
|
||||
/// Insert a fake CompactBlock into the cache DB.
|
||||
pub(crate) fn insert_into_cache<P: AsRef<Path>>(db_cache: P, cb: &CompactBlock) {
|
||||
pub(crate) fn insert_into_cache(db_cache: &CacheConnection, cb: &CompactBlock) {
|
||||
let cb_bytes = cb.write_to_bytes().unwrap();
|
||||
let cache = Connection::open(&db_cache).unwrap();
|
||||
cache
|
||||
db_cache
|
||||
.0
|
||||
.prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)")
|
||||
.unwrap()
|
||||
.execute(&[
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
//! Functions for querying information in the data database.
|
||||
|
||||
use rusqlite::Connection;
|
||||
use std::path::Path;
|
||||
use zcash_primitives::{note_encryption::Memo, transaction::components::Amount};
|
||||
|
||||
use crate::{
|
||||
error::{Error, ErrorKind},
|
||||
get_target_and_anchor_heights,
|
||||
};
|
||||
use zcash_client_backend::data_api::error::Error;
|
||||
|
||||
use crate::{error::SqliteClientError, get_target_and_anchor_heights, DataConnection};
|
||||
|
||||
/// Returns the address for the account.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::query::get_address;
|
||||
/// use tempfile::NamedTempFile;
|
||||
/// use zcash_client_sqlite::{
|
||||
/// DataConnection,
|
||||
/// query::get_address,
|
||||
/// };
|
||||
///
|
||||
/// let addr = get_address("/path/to/data.db", 0);
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db = DataConnection::for_path(data_file).unwrap();
|
||||
/// let addr = get_address(&db, 0);
|
||||
/// ```
|
||||
pub fn get_address<P: AsRef<Path>>(db_data: P, account: u32) -> Result<String, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
let addr = data.query_row(
|
||||
pub fn get_address(data: &DataConnection, account: u32) -> Result<String, rusqlite::Error> {
|
||||
let addr = data.0.query_row(
|
||||
"SELECT address FROM accounts
|
||||
WHERE account = ?",
|
||||
&[account],
|
||||
|
@ -42,14 +43,18 @@ pub fn get_address<P: AsRef<Path>>(db_data: P, account: u32) -> Result<String, E
|
|||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::query::get_balance;
|
||||
/// use tempfile::NamedTempFile;
|
||||
/// use zcash_client_sqlite::{
|
||||
/// DataConnection,
|
||||
/// query::get_balance,
|
||||
/// };
|
||||
///
|
||||
/// let addr = get_balance("/path/to/data.db", 0);
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db = DataConnection::for_path(data_file).unwrap();
|
||||
/// let addr = get_balance(&db, 0);
|
||||
/// ```
|
||||
pub fn get_balance<P: AsRef<Path>>(db_data: P, account: u32) -> Result<Amount, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
let balance = data.query_row(
|
||||
pub fn get_balance(data: &DataConnection, account: u32) -> Result<Amount, SqliteClientError> {
|
||||
let balance = data.0.query_row(
|
||||
"SELECT SUM(value) FROM received_notes
|
||||
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
||||
WHERE account = ? AND spent IS NULL AND transactions.block IS NOT NULL",
|
||||
|
@ -59,7 +64,7 @@ pub fn get_balance<P: AsRef<Path>>(db_data: P, account: u32) -> Result<Amount, E
|
|||
|
||||
match Amount::from_i64(balance) {
|
||||
Ok(amount) if !amount.is_negative() => Ok(amount),
|
||||
_ => Err(Error(ErrorKind::CorruptedData(
|
||||
_ => Err(SqliteClientError(Error::CorruptedData(
|
||||
"Sum of values in received_notes is out of range",
|
||||
))),
|
||||
}
|
||||
|
@ -71,16 +76,23 @@ pub fn get_balance<P: AsRef<Path>>(db_data: P, account: u32) -> Result<Amount, E
|
|||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::query::get_verified_balance;
|
||||
/// use tempfile::NamedTempFile;
|
||||
/// use zcash_client_sqlite::{
|
||||
/// DataConnection,
|
||||
/// query::get_verified_balance,
|
||||
/// };
|
||||
///
|
||||
/// let addr = get_verified_balance("/path/to/data.db", 0);
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db = DataConnection::for_path(data_file).unwrap();
|
||||
/// let addr = get_verified_balance(&db, 0);
|
||||
/// ```
|
||||
pub fn get_verified_balance<P: AsRef<Path>>(db_data: P, account: u32) -> Result<Amount, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
pub fn get_verified_balance(
|
||||
data: &DataConnection,
|
||||
account: u32,
|
||||
) -> Result<Amount, SqliteClientError> {
|
||||
let (_, anchor_height) = get_target_and_anchor_heights(&data)?;
|
||||
|
||||
let balance = data.query_row(
|
||||
let balance = data.0.query_row(
|
||||
"SELECT SUM(value) FROM received_notes
|
||||
INNER JOIN transactions ON transactions.id_tx = received_notes.tx
|
||||
WHERE account = ? AND spent IS NULL AND transactions.block <= ?",
|
||||
|
@ -90,7 +102,7 @@ pub fn get_verified_balance<P: AsRef<Path>>(db_data: P, account: u32) -> Result<
|
|||
|
||||
match Amount::from_i64(balance) {
|
||||
Ok(amount) if !amount.is_negative() => Ok(amount),
|
||||
_ => Err(Error(ErrorKind::CorruptedData(
|
||||
_ => Err(SqliteClientError(Error::CorruptedData(
|
||||
"Sum of values in received_notes is out of range",
|
||||
))),
|
||||
}
|
||||
|
@ -104,16 +116,21 @@ pub fn get_verified_balance<P: AsRef<Path>>(db_data: P, account: u32) -> Result<
|
|||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::query::get_received_memo_as_utf8;
|
||||
/// use tempfile::NamedTempFile;
|
||||
/// use zcash_client_sqlite::{
|
||||
/// DataConnection,
|
||||
/// query::get_received_memo_as_utf8,
|
||||
/// };
|
||||
///
|
||||
/// let memo = get_received_memo_as_utf8("/path/to/data.db", 27);
|
||||
pub fn get_received_memo_as_utf8<P: AsRef<Path>>(
|
||||
db_data: P,
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db = DataConnection::for_path(data_file).unwrap();
|
||||
/// let memo = get_received_memo_as_utf8(&db, 27);
|
||||
/// ```
|
||||
pub fn get_received_memo_as_utf8(
|
||||
data: &DataConnection,
|
||||
id_note: i64,
|
||||
) -> Result<Option<String>, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
let memo: Vec<_> = data.query_row(
|
||||
) -> Result<Option<String>, SqliteClientError> {
|
||||
let memo: Vec<_> = data.0.query_row(
|
||||
"SELECT memo FROM received_notes
|
||||
WHERE id_note = ?",
|
||||
&[id_note],
|
||||
|
@ -123,7 +140,7 @@ pub fn get_received_memo_as_utf8<P: AsRef<Path>>(
|
|||
match Memo::from_bytes(&memo) {
|
||||
Some(memo) => match memo.to_utf8() {
|
||||
Some(Ok(res)) => Ok(Some(res)),
|
||||
Some(Err(e)) => Err(Error(ErrorKind::InvalidMemo(e))),
|
||||
Some(Err(e)) => Err(SqliteClientError(Error::InvalidMemo(e))),
|
||||
None => Ok(None),
|
||||
},
|
||||
None => Ok(None),
|
||||
|
@ -138,16 +155,21 @@ pub fn get_received_memo_as_utf8<P: AsRef<Path>>(
|
|||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use zcash_client_sqlite::query::get_sent_memo_as_utf8;
|
||||
/// use tempfile::NamedTempFile;
|
||||
/// use zcash_client_sqlite::{
|
||||
/// DataConnection,
|
||||
/// query::get_sent_memo_as_utf8,
|
||||
/// };
|
||||
///
|
||||
/// let memo = get_sent_memo_as_utf8("/path/to/data.db", 12);
|
||||
pub fn get_sent_memo_as_utf8<P: AsRef<Path>>(
|
||||
db_data: P,
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db = DataConnection::for_path(data_file).unwrap();
|
||||
/// let memo = get_sent_memo_as_utf8(&db, 12);
|
||||
/// ```
|
||||
pub fn get_sent_memo_as_utf8(
|
||||
data: &DataConnection,
|
||||
id_note: i64,
|
||||
) -> Result<Option<String>, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
let memo: Vec<_> = data.query_row(
|
||||
) -> Result<Option<String>, SqliteClientError> {
|
||||
let memo: Vec<_> = data.0.query_row(
|
||||
"SELECT memo FROM sent_notes
|
||||
WHERE id_note = ?",
|
||||
&[id_note],
|
||||
|
@ -157,7 +179,7 @@ pub fn get_sent_memo_as_utf8<P: AsRef<Path>>(
|
|||
match Memo::from_bytes(&memo) {
|
||||
Some(memo) => match memo.to_utf8() {
|
||||
Some(Ok(res)) => Ok(Some(res)),
|
||||
Some(Err(e)) => Err(Error(ErrorKind::InvalidMemo(e))),
|
||||
Some(Err(e)) => Err(SqliteClientError(Error::InvalidMemo(e))),
|
||||
None => Ok(None),
|
||||
},
|
||||
None => Ok(None),
|
||||
|
@ -166,23 +188,27 @@ pub fn get_sent_memo_as_utf8<P: AsRef<Path>>(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rusqlite::Connection;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use zcash_primitives::{
|
||||
transaction::components::Amount,
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use super::{get_address, get_balance, get_verified_balance};
|
||||
use zcash_client_backend::data_api::error::Error;
|
||||
|
||||
use crate::{
|
||||
error::ErrorKind,
|
||||
init::{init_accounts_table, init_data_database},
|
||||
tests,
|
||||
tests, DataConnection,
|
||||
};
|
||||
|
||||
use super::{get_address, get_balance, get_verified_balance};
|
||||
|
||||
#[test]
|
||||
fn empty_database_has_no_balance() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -191,17 +217,17 @@ mod tests {
|
|||
init_accounts_table(&db_data, &tests::network(), &extfvks).unwrap();
|
||||
|
||||
// The account should be empty
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), Amount::zero());
|
||||
|
||||
// The account should have no verified balance, as we haven't scanned any blocks
|
||||
let e = get_verified_balance(db_data, 0).unwrap_err();
|
||||
match e.kind() {
|
||||
ErrorKind::ScanRequired => (),
|
||||
let e = get_verified_balance(&db_data, 0).unwrap_err();
|
||||
match e.0 {
|
||||
Error::ScanRequired => (),
|
||||
_ => panic!("Unexpected error: {:?}", e),
|
||||
}
|
||||
|
||||
// An invalid account has zero balance
|
||||
assert!(get_address(db_data, 1).is_err());
|
||||
assert_eq!(get_balance(db_data, 1).unwrap(), Amount::zero());
|
||||
assert!(get_address(&db_data, 1).is_err());
|
||||
assert_eq!(get_balance(&db_data, 1).unwrap(), Amount::zero());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
//! Functions for scanning the chain and extracting relevant information.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use ff::PrimeField;
|
||||
use protobuf::parse_from_bytes;
|
||||
use rusqlite::{types::ToSql, Connection, OptionalExtension, NO_PARAMS};
|
||||
|
||||
use rusqlite::{types::ToSql, OptionalExtension, NO_PARAMS};
|
||||
|
||||
use zcash_client_backend::{
|
||||
address::RecipientAddress, decrypt_transaction, encoding::decode_extended_full_viewing_key,
|
||||
proto::compact_formats::CompactBlock, welding_rig::scan_block,
|
||||
address::RecipientAddress,
|
||||
data_api::error::{ChainInvalid, Error},
|
||||
decrypt_transaction,
|
||||
encoding::decode_extended_full_viewing_key,
|
||||
proto::compact_formats::CompactBlock,
|
||||
welding_rig::scan_block,
|
||||
};
|
||||
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
|
@ -17,7 +21,7 @@ use zcash_primitives::{
|
|||
transaction::Transaction,
|
||||
};
|
||||
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::{error::SqliteClientError, CacheConnection, DataConnection};
|
||||
|
||||
struct CompactBlockRow {
|
||||
height: BlockHeight,
|
||||
|
@ -48,44 +52,52 @@ struct WitnessRow {
|
|||
/// [`init_blocks_table`] before this function.
|
||||
///
|
||||
/// Scanned blocks are required to be height-sequential. If a block is missing from the
|
||||
/// cache, an error will be returned with kind [`ErrorKind::InvalidHeight`].
|
||||
/// cache, an error will be returned with kind [`ChainInvalid::HeightMismatch`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tempfile::NamedTempFile;
|
||||
/// use zcash_primitives::consensus::{
|
||||
/// Network,
|
||||
/// Parameters,
|
||||
/// };
|
||||
/// use zcash_client_sqlite::scan::scan_cached_blocks;
|
||||
/// use zcash_client_sqlite::{
|
||||
/// CacheConnection,
|
||||
/// DataConnection,
|
||||
/// scan::scan_cached_blocks,
|
||||
/// };
|
||||
///
|
||||
/// scan_cached_blocks(&Network::TestNetwork, "/path/to/cache.db", "/path/to/data.db", None);
|
||||
/// let cache_file = NamedTempFile::new().unwrap();
|
||||
/// let cache = CacheConnection::for_path(cache_file).unwrap();
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let data = DataConnection::for_path(data_file).unwrap();
|
||||
/// scan_cached_blocks(&Network::TestNetwork, &cache, &data, None);
|
||||
/// ```
|
||||
///
|
||||
/// [`init_blocks_table`]: crate::init::init_blocks_table
|
||||
pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRef<Path>>(
|
||||
params: &Params,
|
||||
db_cache: P,
|
||||
db_data: Q,
|
||||
pub fn scan_cached_blocks<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
cache: &CacheConnection,
|
||||
data: &DataConnection,
|
||||
limit: Option<u32>,
|
||||
) -> Result<(), Error> {
|
||||
let cache = Connection::open(db_cache)?;
|
||||
let data = Connection::open(db_data)?;
|
||||
) -> Result<(), SqliteClientError> {
|
||||
let sapling_activation_height = params
|
||||
.activation_height(NetworkUpgrade::Sapling)
|
||||
.ok_or(Error(ErrorKind::SaplingNotActive))?;
|
||||
.ok_or(Error::SaplingNotActive)?;
|
||||
|
||||
// Recall where we synced up to previously.
|
||||
// If we have never synced, use sapling activation height to select all cached CompactBlocks.
|
||||
let mut last_height: BlockHeight =
|
||||
data.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
|
||||
row.get::<_, u32>(0)
|
||||
.map(BlockHeight::from)
|
||||
.or(Ok(sapling_activation_height - 1))
|
||||
})?;
|
||||
data.0
|
||||
.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
|
||||
row.get::<_, u32>(0)
|
||||
.map(BlockHeight::from)
|
||||
.or(Ok(sapling_activation_height - 1))
|
||||
})?;
|
||||
|
||||
// Fetch the CompactBlocks we need to scan
|
||||
let mut stmt_blocks = cache.prepare(
|
||||
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(
|
||||
|
@ -99,8 +111,9 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
)?;
|
||||
|
||||
// Fetch the ExtendedFullViewingKeys we are tracking
|
||||
let mut stmt_fetch_accounts =
|
||||
data.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
|
||||
let mut stmt_fetch_accounts = data
|
||||
.0
|
||||
.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
|
||||
let extfvks = stmt_fetch_accounts.query_map(NO_PARAMS, |row| {
|
||||
row.get(0).map(|extfvk: String| {
|
||||
decode_extended_full_viewing_key(
|
||||
|
@ -112,10 +125,12 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
// Raise SQL errors from the query, IO errors from parsing, and incorrect HRP errors.
|
||||
let extfvks: Vec<_> = extfvks
|
||||
.collect::<Result<Result<Option<_>, _>, _>>()??
|
||||
.ok_or(Error(ErrorKind::IncorrectHRPExtFVK))?;
|
||||
.ok_or(Error::IncorrectHRPExtFVK)?;
|
||||
|
||||
// Get the most recent CommitmentTree
|
||||
let mut stmt_fetch_tree = data.prepare("SELECT sapling_tree FROM blocks WHERE height = ?")?;
|
||||
let mut stmt_fetch_tree = data
|
||||
.0
|
||||
.prepare("SELECT sapling_tree FROM blocks WHERE height = ?")?;
|
||||
let mut tree = stmt_fetch_tree
|
||||
.query_row(&[u32::from(last_height)], |row| {
|
||||
row.get(0).map(|data: Vec<_>| {
|
||||
|
@ -125,8 +140,9 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
.unwrap_or_else(|_| CommitmentTree::new());
|
||||
|
||||
// Get most recent incremental witnesses for the notes we are tracking
|
||||
let mut stmt_fetch_witnesses =
|
||||
data.prepare("SELECT note, witness FROM sapling_witnesses WHERE block = ?")?;
|
||||
let mut stmt_fetch_witnesses = data
|
||||
.0
|
||||
.prepare("SELECT note, witness FROM sapling_witnesses WHERE block = ?")?;
|
||||
let witnesses = stmt_fetch_witnesses.query_map(&[u32::from(last_height)], |row| {
|
||||
let id_note = row.get(0)?;
|
||||
let data: Vec<_> = row.get(1)?;
|
||||
|
@ -135,8 +151,9 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
let mut witnesses: Vec<_> = witnesses.collect::<Result<Result<_, _>, _>>()??;
|
||||
|
||||
// Get the nullifiers for the notes we are tracking
|
||||
let mut stmt_fetch_nullifiers =
|
||||
data.prepare("SELECT id_note, nf, account FROM received_notes WHERE spent IS NULL")?;
|
||||
let mut stmt_fetch_nullifiers = data
|
||||
.0
|
||||
.prepare("SELECT id_note, nf, account FROM received_notes WHERE spent IS NULL")?;
|
||||
let nullifiers = stmt_fetch_nullifiers.query_map(NO_PARAMS, |row| {
|
||||
let nf: Vec<_> = row.get(1)?;
|
||||
let account: i64 = row.get(2)?;
|
||||
|
@ -145,38 +162,44 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
let mut nullifiers: Vec<_> = nullifiers.collect::<Result<_, _>>()?;
|
||||
|
||||
// Prepare per-block SQL statements
|
||||
let mut stmt_insert_block = data.prepare(
|
||||
let mut stmt_insert_block = data.0.prepare(
|
||||
"INSERT INTO blocks (height, hash, time, sapling_tree)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_update_tx = data.prepare(
|
||||
let mut stmt_update_tx = data.0.prepare(
|
||||
"UPDATE transactions
|
||||
SET block = ?, tx_index = ? WHERE txid = ?",
|
||||
)?;
|
||||
let mut stmt_insert_tx = data.prepare(
|
||||
let mut stmt_insert_tx = data.0.prepare(
|
||||
"INSERT INTO transactions (txid, block, tx_index)
|
||||
VALUES (?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_select_tx = data.prepare("SELECT id_tx FROM transactions WHERE txid = ?")?;
|
||||
let mut stmt_mark_spent_note =
|
||||
data.prepare("UPDATE received_notes SET spent = ? WHERE nf = ?")?;
|
||||
let mut stmt_update_note = data.prepare(
|
||||
let mut stmt_select_tx = data
|
||||
.0
|
||||
.prepare("SELECT id_tx FROM transactions WHERE txid = ?")?;
|
||||
let mut stmt_mark_spent_note = data
|
||||
.0
|
||||
.prepare("UPDATE received_notes SET spent = ? WHERE nf = ?")?;
|
||||
let mut stmt_update_note = data.0.prepare(
|
||||
"UPDATE received_notes
|
||||
SET account = ?, diversifier = ?, value = ?, rcm = ?, nf = ?, is_change = ?
|
||||
WHERE tx = ? AND output_index = ?",
|
||||
)?;
|
||||
let mut stmt_insert_note = data.prepare(
|
||||
let mut stmt_insert_note = data.0.prepare(
|
||||
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_select_note =
|
||||
data.prepare("SELECT id_note FROM received_notes WHERE tx = ? AND output_index = ?")?;
|
||||
let mut stmt_insert_witness = data.prepare(
|
||||
let mut stmt_select_note = data
|
||||
.0
|
||||
.prepare("SELECT id_note FROM received_notes WHERE tx = ? AND output_index = ?")?;
|
||||
let mut stmt_insert_witness = data.0.prepare(
|
||||
"INSERT INTO sapling_witnesses (note, block, witness)
|
||||
VALUES (?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_prune_witnesses = data.prepare("DELETE FROM sapling_witnesses WHERE block < ?")?;
|
||||
let mut stmt_update_expired = data.prepare(
|
||||
let mut stmt_prune_witnesses = data
|
||||
.0
|
||||
.prepare("DELETE FROM sapling_witnesses WHERE block < ?")?;
|
||||
let mut stmt_update_expired = data.0.prepare(
|
||||
"UPDATE received_notes SET spent = NULL WHERE EXISTS (
|
||||
SELECT id_tx FROM transactions
|
||||
WHERE id_tx = received_notes.spent AND block IS NULL AND expiry_height < ?
|
||||
|
@ -187,11 +210,14 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
let row = row?;
|
||||
|
||||
// Start an SQL transaction for this block.
|
||||
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
data.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
|
||||
// Scanned blocks MUST be height-sequential.
|
||||
if row.height != (last_height + 1) {
|
||||
return Err(Error(ErrorKind::InvalidHeight(last_height + 1, row.height)));
|
||||
return Err(SqliteClientError(ChainInvalid::block_height_mismatch(
|
||||
last_height + 1,
|
||||
row.height,
|
||||
)));
|
||||
}
|
||||
last_height = row.height;
|
||||
|
||||
|
@ -218,7 +244,7 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
let cur_root = tree.root();
|
||||
for row in &witnesses {
|
||||
if row.witness.root() != cur_root {
|
||||
return Err(Error(ErrorKind::InvalidWitnessAnchor(
|
||||
return Err(SqliteClientError(Error::InvalidWitnessAnchor(
|
||||
row.id_note,
|
||||
last_height,
|
||||
)));
|
||||
|
@ -227,12 +253,13 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
for tx in &txs {
|
||||
for output in tx.shielded_outputs.iter() {
|
||||
if output.witness.root() != cur_root {
|
||||
return Err(Error(ErrorKind::InvalidNewWitnessAnchor(
|
||||
return Err(Error::InvalidNewWitnessAnchor(
|
||||
output.index,
|
||||
tx.txid,
|
||||
last_height,
|
||||
output.witness.root(),
|
||||
)));
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -264,7 +291,7 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
u32::from(row.height).to_sql()?,
|
||||
(tx.index as i64).to_sql()?,
|
||||
])?;
|
||||
data.last_insert_rowid()
|
||||
data.0.last_insert_rowid()
|
||||
} else {
|
||||
// It was there, so grab its row number.
|
||||
stmt_select_tx.query_row(&[txid], |row| row.get(0))?
|
||||
|
@ -318,7 +345,7 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
nf.to_sql()?,
|
||||
output.is_change.to_sql()?,
|
||||
])?;
|
||||
data.last_insert_rowid()
|
||||
data.0.last_insert_rowid()
|
||||
} else {
|
||||
// It was there, so grab its row number.
|
||||
stmt_select_note.query_row(
|
||||
|
@ -360,7 +387,7 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
stmt_update_expired.execute(&[u32::from(last_height)])?;
|
||||
|
||||
// Commit the SQL transaction, writing this block's data atomically.
|
||||
data.execute("COMMIT", NO_PARAMS)?;
|
||||
data.0.execute("COMMIT", NO_PARAMS)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -368,16 +395,16 @@ pub fn scan_cached_blocks<Params: consensus::Parameters, P: AsRef<Path>, Q: AsRe
|
|||
|
||||
/// Scans a [`Transaction`] for any information that can be decrypted by the accounts in
|
||||
/// the wallet, and saves it to the wallet.
|
||||
pub fn decrypt_and_store_transaction<D: AsRef<Path>, P: consensus::Parameters>(
|
||||
db_data: D,
|
||||
pub fn decrypt_and_store_transaction<P: consensus::Parameters>(
|
||||
data: &DataConnection,
|
||||
params: &P,
|
||||
tx: &Transaction,
|
||||
) -> Result<(), Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
) -> Result<(), SqliteClientError> {
|
||||
// Fetch the ExtendedFullViewingKeys we are tracking
|
||||
let mut stmt_fetch_accounts =
|
||||
data.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
|
||||
let mut stmt_fetch_accounts = data
|
||||
.0
|
||||
.prepare("SELECT extfvk FROM accounts ORDER BY account ASC")?;
|
||||
|
||||
let extfvks = stmt_fetch_accounts.query_map(NO_PARAMS, |row| {
|
||||
row.get(0).map(|extfvk: String| {
|
||||
decode_extended_full_viewing_key(
|
||||
|
@ -386,13 +413,16 @@ pub fn decrypt_and_store_transaction<D: AsRef<Path>, P: consensus::Parameters>(
|
|||
)
|
||||
})
|
||||
})?;
|
||||
|
||||
// Raise SQL errors from the query, IO errors from parsing, and incorrect HRP errors.
|
||||
let extfvks: Vec<_> = extfvks
|
||||
.collect::<Result<Result<Option<_>, _>, _>>()??
|
||||
.ok_or(Error(ErrorKind::IncorrectHRPExtFVK))?;
|
||||
.ok_or(SqliteClientError(Error::IncorrectHRPExtFVK))?;
|
||||
|
||||
// Height is block height for mined transactions, and the "mempool height" (chain height + 1) for mempool transactions.
|
||||
let mut stmt_select_block = data.prepare("SELECT block FROM transactions WHERE txid = ?")?;
|
||||
let mut stmt_select_block = data
|
||||
.0
|
||||
.prepare("SELECT block FROM transactions WHERE txid = ?")?;
|
||||
let height = match stmt_select_block
|
||||
.query_row(&[tx.txid().0.to_vec()], |row| {
|
||||
row.get::<_, u32>(0).map(BlockHeight::from)
|
||||
|
@ -401,13 +431,14 @@ pub fn decrypt_and_store_transaction<D: AsRef<Path>, P: consensus::Parameters>(
|
|||
{
|
||||
Some(height) => height,
|
||||
None => data
|
||||
.0
|
||||
.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
|
||||
row.get(0)
|
||||
})
|
||||
.optional()?
|
||||
.map(|last_height: u32| BlockHeight::from(last_height + 1))
|
||||
.or_else(|| params.activation_height(NetworkUpgrade::Sapling))
|
||||
.ok_or(Error(ErrorKind::SaplingNotActive))?,
|
||||
.ok_or(SqliteClientError(Error::SaplingNotActive))?,
|
||||
};
|
||||
|
||||
let outputs = decrypt_transaction(params, height, tx, &extfvks);
|
||||
|
@ -417,36 +448,38 @@ pub fn decrypt_and_store_transaction<D: AsRef<Path>, P: consensus::Parameters>(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let mut stmt_update_tx = data.prepare(
|
||||
let mut stmt_update_tx = data.0.prepare(
|
||||
"UPDATE transactions
|
||||
SET expiry_height = ?, raw = ? WHERE txid = ?",
|
||||
)?;
|
||||
let mut stmt_insert_tx = data.prepare(
|
||||
let mut stmt_insert_tx = data.0.prepare(
|
||||
"INSERT INTO transactions (txid, expiry_height, raw)
|
||||
VALUES (?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_select_tx = data.prepare("SELECT id_tx FROM transactions WHERE txid = ?")?;
|
||||
let mut stmt_update_sent_note = data.prepare(
|
||||
let mut stmt_select_tx = data
|
||||
.0
|
||||
.prepare("SELECT id_tx FROM transactions WHERE txid = ?")?;
|
||||
let mut stmt_update_sent_note = data.0.prepare(
|
||||
"UPDATE sent_notes
|
||||
SET from_account = ?, address = ?, value = ?, memo = ?
|
||||
WHERE tx = ? AND output_index = ?",
|
||||
)?;
|
||||
let mut stmt_insert_sent_note = data.prepare(
|
||||
let mut stmt_insert_sent_note = data.0.prepare(
|
||||
"INSERT INTO sent_notes (tx, output_index, from_account, address, value, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
let mut stmt_update_received_note = data.prepare(
|
||||
let mut stmt_update_received_note = data.0.prepare(
|
||||
"UPDATE received_notes
|
||||
SET account = ?, diversifier = ?, value = ?, rcm = ?, memo = ?
|
||||
WHERE tx = ? AND output_index = ?",
|
||||
)?;
|
||||
let mut stmt_insert_received_note = data.prepare(
|
||||
let mut stmt_insert_received_note = data.0.prepare(
|
||||
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
|
||||
// Update the database atomically, to ensure the result is internally consistent.
|
||||
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
data.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
|
||||
// First try update an existing transaction in the database.
|
||||
let txid = tx.txid().0.to_vec();
|
||||
|
@ -464,7 +497,7 @@ pub fn decrypt_and_store_transaction<D: AsRef<Path>, P: consensus::Parameters>(
|
|||
u32::from(tx.expiry_height).to_sql()?,
|
||||
raw_tx.to_sql()?,
|
||||
])?;
|
||||
data.last_insert_rowid()
|
||||
data.0.last_insert_rowid()
|
||||
} else {
|
||||
// It was there, so grab its row number.
|
||||
stmt_select_tx.query_row(&[txid], |row| row.get(0))?
|
||||
|
@ -526,21 +559,25 @@ pub fn decrypt_and_store_transaction<D: AsRef<Path>, P: consensus::Parameters>(
|
|||
}
|
||||
}
|
||||
|
||||
data.execute("COMMIT", NO_PARAMS)?;
|
||||
data.0.execute("COMMIT", NO_PARAMS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rusqlite::{Connection, NO_PARAMS};
|
||||
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
transaction::components::Amount,
|
||||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use super::scan_cached_blocks;
|
||||
use zcash_client_backend::data_api::error::ChainInvalid;
|
||||
|
||||
use crate::{
|
||||
init::{init_accounts_table, init_cache_database, init_data_database},
|
||||
query::get_balance,
|
||||
|
@ -548,16 +585,19 @@ mod tests {
|
|||
self, fake_compact_block, fake_compact_block_spending, insert_into_cache,
|
||||
sapling_activation_height,
|
||||
},
|
||||
CacheConnection, DataConnection,
|
||||
};
|
||||
|
||||
use super::scan_cached_blocks;
|
||||
|
||||
#[test]
|
||||
fn scan_cached_blocks_requires_sequential_blocks() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
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 = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -573,9 +613,9 @@ mod tests {
|
|||
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, 0).unwrap(), value);
|
||||
insert_into_cache(&db_cache, &cb1);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), value);
|
||||
|
||||
// We cannot scan a block of height SAPLING_ACTIVATION_HEIGHT + 2 next
|
||||
let (cb2, _) = fake_compact_block(
|
||||
|
@ -590,24 +630,31 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb3);
|
||||
match scan_cached_blocks(&tests::network(), db_cache, db_data, None) {
|
||||
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(),
|
||||
format!(
|
||||
"Expected height of next CompactBlock to be {}, but was {}",
|
||||
sapling_activation_height() + 1,
|
||||
sapling_activation_height() + 2
|
||||
)
|
||||
),
|
||||
Err(e) => {
|
||||
assert_eq!(
|
||||
e.0.to_string(),
|
||||
ChainInvalid::block_height_mismatch::<rusqlite::Error>(
|
||||
sapling_activation_height() + 1,
|
||||
sapling_activation_height() + 2
|
||||
)
|
||||
.to_string()
|
||||
);
|
||||
|
||||
//FIXME: scan_cached_blocks is leaving the database in an invalid
|
||||
//transactional state on error; this rollback should be intrinsic
|
||||
//to the failure path.
|
||||
db_data.0.execute("ROLLBACK", NO_PARAMS).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
insert_into_cache(&db_cache, &cb2);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
assert_eq!(
|
||||
get_balance(db_data, 0).unwrap(),
|
||||
get_balance(&db_data, 0).unwrap(),
|
||||
Amount::from_u64(150_000).unwrap()
|
||||
);
|
||||
}
|
||||
|
@ -615,11 +662,11 @@ mod tests {
|
|||
#[test]
|
||||
fn scan_cached_blocks_finds_received_notes() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
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 = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -628,7 +675,7 @@ mod tests {
|
|||
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), Amount::zero());
|
||||
|
||||
// Create a fake CompactBlock sending value to the address
|
||||
let value = Amount::from_u64(5).unwrap();
|
||||
|
@ -638,35 +685,35 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect the received note
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
assert_eq!(get_balance(&db_data, 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);
|
||||
insert_into_cache(&db_cache, &cb2);
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect both received notes
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value + value2);
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), value + value2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_cached_blocks_finds_change_notes() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
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 = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -675,7 +722,7 @@ mod tests {
|
|||
init_accounts_table(&db_data, &tests::network(), &[extfvk.clone()]).unwrap();
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), Amount::zero());
|
||||
|
||||
// Create a fake CompactBlock sending value to the address
|
||||
let value = Amount::from_u64(5).unwrap();
|
||||
|
@ -685,20 +732,20 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
|
||||
// Scan the cache
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Account balance should reflect the received note
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
assert_eq!(get_balance(&db_data, 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,
|
||||
&db_cache,
|
||||
&fake_compact_block_spending(
|
||||
sapling_activation_height() + 1,
|
||||
cb.hash(),
|
||||
|
@ -710,9 +757,9 @@ mod tests {
|
|||
);
|
||||
|
||||
// Scan the cache again
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Account balance should equal the change
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value - value2);
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), value - value2);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
//! Functions for creating transactions.
|
||||
|
||||
use ff::PrimeField;
|
||||
use rusqlite::{types::ToSql, Connection, NO_PARAMS};
|
||||
use rusqlite::{types::ToSql, NO_PARAMS};
|
||||
use std::convert::TryInto;
|
||||
use std::path::Path;
|
||||
use zcash_client_backend::{address::RecipientAddress, encoding::encode_extended_full_viewing_key};
|
||||
use zcash_client_backend::{
|
||||
address::RecipientAddress, data_api::error::Error, encoding::encode_extended_full_viewing_key,
|
||||
};
|
||||
use zcash_primitives::{
|
||||
consensus,
|
||||
keys::OutgoingViewingKey,
|
||||
|
@ -20,10 +21,7 @@ use zcash_primitives::{
|
|||
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::{Error, ErrorKind},
|
||||
get_target_and_anchor_heights,
|
||||
};
|
||||
use crate::{error::SqliteClientError, get_target_and_anchor_heights, DataConnection};
|
||||
|
||||
/// Describes a policy for which outgoing viewing key should be able to decrypt
|
||||
/// transaction outputs.
|
||||
|
@ -86,6 +84,7 @@ struct SelectedNoteRow {
|
|||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use tempfile::NamedTempFile;
|
||||
/// use zcash_primitives::{
|
||||
/// consensus::{self, Network},
|
||||
/// constants::testnet::COIN_TYPE,
|
||||
|
@ -95,7 +94,10 @@ struct SelectedNoteRow {
|
|||
/// use zcash_client_backend::{
|
||||
/// keys::spending_key,
|
||||
/// };
|
||||
/// use zcash_client_sqlite::transact::{create_to_address, OvkPolicy};
|
||||
/// use zcash_client_sqlite::{
|
||||
/// DataConnection,
|
||||
/// transact::{create_to_address, OvkPolicy},
|
||||
/// };
|
||||
///
|
||||
/// let tx_prover = match LocalTxProver::with_default_location() {
|
||||
/// Some(tx_prover) => tx_prover,
|
||||
|
@ -107,8 +109,11 @@ struct SelectedNoteRow {
|
|||
/// let account = 0;
|
||||
/// let extsk = spending_key(&[0; 32][..], COIN_TYPE, account);
|
||||
/// let to = extsk.default_address().unwrap().1.into();
|
||||
///
|
||||
/// let data_file = NamedTempFile::new().unwrap();
|
||||
/// let db = DataConnection::for_path(data_file).unwrap();
|
||||
/// match create_to_address(
|
||||
/// "/path/to/data.db",
|
||||
/// &db,
|
||||
/// &Network::TestNetwork,
|
||||
/// consensus::BranchId::Sapling,
|
||||
/// tx_prover,
|
||||
|
@ -122,8 +127,8 @@ struct SelectedNoteRow {
|
|||
/// Err(e) => (),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
||||
db_data: DB,
|
||||
pub fn create_to_address<P: consensus::Parameters>(
|
||||
data: &DataConnection,
|
||||
params: &P,
|
||||
consensus_branch_id: consensus::BranchId,
|
||||
prover: impl TxProver,
|
||||
|
@ -132,13 +137,12 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
value: Amount,
|
||||
memo: Option<Memo>,
|
||||
ovk_policy: OvkPolicy,
|
||||
) -> Result<i64, Error> {
|
||||
let data = Connection::open(db_data)?;
|
||||
|
||||
) -> Result<i64, SqliteClientError> {
|
||||
// Check that the ExtendedSpendingKey we have been given corresponds to the
|
||||
// ExtendedFullViewingKey for the account we are spending from.
|
||||
let extfvk = ExtendedFullViewingKey::from(extsk);
|
||||
if !data
|
||||
.0
|
||||
.prepare("SELECT * FROM accounts WHERE account = ? AND extfvk = ?")?
|
||||
.exists(&[
|
||||
account.to_sql()?,
|
||||
|
@ -149,7 +153,7 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
.to_sql()?,
|
||||
])?
|
||||
{
|
||||
return Err(Error(ErrorKind::InvalidExtSK(account)));
|
||||
return Err(Error::InvalidExtSK(account).into());
|
||||
}
|
||||
|
||||
// Apply the outgoing viewing key policy.
|
||||
|
@ -181,7 +185,7 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
//
|
||||
// 4) Match the selected notes against the witnesses at the desired height.
|
||||
let target_value = i64::from(value + DEFAULT_FEE);
|
||||
let mut stmt_select_notes = data.prepare(
|
||||
let mut stmt_select_notes = data.0.prepare(
|
||||
"WITH selected AS (
|
||||
WITH eligible AS (
|
||||
SELECT id_note, diversifier, value, rcm,
|
||||
|
@ -204,7 +208,7 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
)?;
|
||||
|
||||
// Select notes
|
||||
let notes = stmt_select_notes.query_and_then::<_, Error, _, _>(
|
||||
let notes = stmt_select_notes.query_and_then::<_, SqliteClientError, _, _>(
|
||||
&[
|
||||
i64::from(account),
|
||||
i64::from(anchor_height),
|
||||
|
@ -216,7 +220,7 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
let diversifier = {
|
||||
let d: Vec<_> = row.get(0)?;
|
||||
if d.len() != 11 {
|
||||
return Err(Error(ErrorKind::CorruptedData(
|
||||
return Err(SqliteClientError(Error::CorruptedData(
|
||||
"Invalid diversifier length",
|
||||
)));
|
||||
}
|
||||
|
@ -236,9 +240,9 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
let rcm = jubjub::Fr::from_repr(
|
||||
rcm_bytes[..]
|
||||
.try_into()
|
||||
.map_err(|_| Error(ErrorKind::InvalidNote))?,
|
||||
.map_err(|_| SqliteClientError(Error::InvalidNote))?,
|
||||
)
|
||||
.ok_or(Error(ErrorKind::InvalidNote))?;
|
||||
.ok_or(SqliteClientError(Error::InvalidNote))?;
|
||||
Rseed::BeforeZip212(rcm)
|
||||
};
|
||||
|
||||
|
@ -266,10 +270,7 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
.iter()
|
||||
.fold(0, |acc, selected| acc + selected.note.value);
|
||||
if selected_value < target_value as u64 {
|
||||
return Err(Error(ErrorKind::InsufficientBalance(
|
||||
selected_value,
|
||||
target_value as u64,
|
||||
)));
|
||||
return Err(Error::InsufficientBalance(selected_value, target_value as u64).into());
|
||||
}
|
||||
|
||||
// Create the transaction
|
||||
|
@ -297,12 +298,12 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
let created = time::OffsetDateTime::now_utc();
|
||||
|
||||
// Update the database atomically, to ensure the result is internally consistent.
|
||||
data.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
data.0.execute("BEGIN IMMEDIATE", NO_PARAMS)?;
|
||||
|
||||
// Save the transaction in the database.
|
||||
let mut raw_tx = vec![];
|
||||
tx.write(&mut raw_tx)?;
|
||||
let mut stmt_insert_tx = data.prepare(
|
||||
let mut stmt_insert_tx = data.0.prepare(
|
||||
"INSERT INTO transactions (txid, created, expiry_height, raw)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
)?;
|
||||
|
@ -312,7 +313,7 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
i64::from(tx.expiry_height).to_sql()?,
|
||||
raw_tx.to_sql()?,
|
||||
])?;
|
||||
let id_tx = data.last_insert_rowid();
|
||||
let id_tx = data.0.last_insert_rowid();
|
||||
|
||||
// Mark notes as spent.
|
||||
//
|
||||
|
@ -322,8 +323,9 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
//
|
||||
// Assumes that create_to_address() will never be called in parallel, which is a
|
||||
// reasonable assumption for a light client such as a mobile phone.
|
||||
let mut stmt_mark_spent_note =
|
||||
data.prepare("UPDATE received_notes SET spent = ? WHERE nf = ?")?;
|
||||
let mut stmt_mark_spent_note = data
|
||||
.0
|
||||
.prepare("UPDATE received_notes SET spent = ? WHERE nf = ?")?;
|
||||
for spend in &tx.shielded_spends {
|
||||
stmt_mark_spent_note.execute(&[id_tx.to_sql()?, spend.nullifier.to_sql()?])?;
|
||||
}
|
||||
|
@ -332,7 +334,7 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
// TODO: Decide how to save transparent output information.
|
||||
let to_str = to.encode(params);
|
||||
if let Some(memo) = memo {
|
||||
let mut stmt_insert_sent_note = data.prepare(
|
||||
let mut stmt_insert_sent_note = data.0.prepare(
|
||||
"INSERT INTO sent_notes (tx, output_index, from_account, address, value, memo)
|
||||
VALUES (?, ?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
|
@ -345,7 +347,7 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
memo.as_bytes().to_sql()?,
|
||||
])?;
|
||||
} else {
|
||||
let mut stmt_insert_sent_note = data.prepare(
|
||||
let mut stmt_insert_sent_note = data.0.prepare(
|
||||
"INSERT INTO sent_notes (tx, output_index, from_account, address, value)
|
||||
VALUES (?, ?, ?, ?, ?)",
|
||||
)?;
|
||||
|
@ -358,7 +360,7 @@ pub fn create_to_address<DB: AsRef<Path>, P: consensus::Parameters>(
|
|||
])?;
|
||||
}
|
||||
|
||||
data.execute("COMMIT", NO_PARAMS)?;
|
||||
data.0.execute("COMMIT", NO_PARAMS)?;
|
||||
|
||||
// Return the row number of the transaction, so the caller can fetch it for sending.
|
||||
Ok(id_tx)
|
||||
|
@ -385,6 +387,7 @@ mod tests {
|
|||
query::{get_balance, get_verified_balance},
|
||||
scan::scan_cached_blocks,
|
||||
tests::{self, fake_compact_block, insert_into_cache, sapling_activation_height},
|
||||
CacheConnection, DataConnection,
|
||||
};
|
||||
|
||||
use super::{create_to_address, OvkPolicy};
|
||||
|
@ -401,7 +404,7 @@ mod tests {
|
|||
#[test]
|
||||
fn create_to_address_fails_on_incorrect_extsk() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add two accounts to the wallet
|
||||
|
@ -416,7 +419,7 @@ mod tests {
|
|||
|
||||
// Invalid extsk for the given account should cause an error
|
||||
match create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -429,8 +432,9 @@ mod tests {
|
|||
Ok(_) => panic!("Should have failed"),
|
||||
Err(e) => assert_eq!(e.to_string(), "Incorrect ExtendedSpendingKey for account 0"),
|
||||
}
|
||||
|
||||
match create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -448,7 +452,7 @@ mod tests {
|
|||
#[test]
|
||||
fn create_to_address_fails_with_no_blocks() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -459,7 +463,7 @@ mod tests {
|
|||
|
||||
// We cannot do anything if we aren't synchronised
|
||||
match create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -477,7 +481,7 @@ mod tests {
|
|||
#[test]
|
||||
fn create_to_address_fails_on_insufficient_balance() {
|
||||
let data_file = NamedTempFile::new().unwrap();
|
||||
let db_data = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
init_blocks_table(&db_data, 1, BlockHash([1; 32]), 1, &[]).unwrap();
|
||||
|
||||
|
@ -488,11 +492,11 @@ mod tests {
|
|||
let to = extsk.default_address().unwrap().1.into();
|
||||
|
||||
// Account balance should be zero
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), Amount::zero());
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), Amount::zero());
|
||||
|
||||
// We cannot spend anything
|
||||
match create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -513,11 +517,11 @@ mod tests {
|
|||
#[test]
|
||||
fn create_to_address_fails_on_unverified_notes() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
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 = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -533,12 +537,12 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Verified balance matches total balance
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
assert_eq!(get_verified_balance(db_data, 0).unwrap(), value);
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), value);
|
||||
assert_eq!(get_verified_balance(&db_data, 0).unwrap(), value);
|
||||
|
||||
// Add more funds to the wallet in a second note
|
||||
let (cb, _) = fake_compact_block(
|
||||
|
@ -547,18 +551,18 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Verified balance does not include the second note
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value + value);
|
||||
assert_eq!(get_verified_balance(db_data, 0).unwrap(), value);
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), value + value);
|
||||
assert_eq!(get_verified_balance(&db_data, 0).unwrap(), value);
|
||||
|
||||
// Spend fails because there are insufficient verified notes
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
let to = extsk2.default_address().unwrap().1.into();
|
||||
match create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -584,13 +588,13 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
}
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Second spend still fails
|
||||
match create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -614,12 +618,12 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Second spend should now succeed
|
||||
create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -635,11 +639,11 @@ mod tests {
|
|||
#[test]
|
||||
fn create_to_address_fails_on_locked_notes() {
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
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 = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -655,15 +659,15 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), value);
|
||||
|
||||
// Send some of the funds to another address
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
let to = extsk2.default_address().unwrap().1.into();
|
||||
create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -677,7 +681,7 @@ mod tests {
|
|||
|
||||
// A second spend fails because there are no usable notes
|
||||
match create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -703,13 +707,13 @@ mod tests {
|
|||
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
}
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Second spend still fails
|
||||
match create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -733,12 +737,12 @@ mod tests {
|
|||
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[22])),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), db_cache, db_data, None).unwrap();
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
scan_cached_blocks(&tests::network(), &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Second spend should now succeed
|
||||
create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&tests::network(),
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -755,11 +759,11 @@ mod tests {
|
|||
fn ovk_policy_prevents_recovery_from_chain() {
|
||||
let network = tests::network();
|
||||
let cache_file = NamedTempFile::new().unwrap();
|
||||
let db_cache = cache_file.path();
|
||||
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 = data_file.path();
|
||||
let db_data = DataConnection(Connection::open(data_file.path()).unwrap());
|
||||
init_data_database(&db_data).unwrap();
|
||||
|
||||
// Add an account to the wallet
|
||||
|
@ -775,9 +779,9 @@ mod tests {
|
|||
extfvk.clone(),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
scan_cached_blocks(&network, db_cache, db_data, None).unwrap();
|
||||
assert_eq!(get_balance(db_data, 0).unwrap(), value);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
scan_cached_blocks(&network, &db_cache, &db_data, None).unwrap();
|
||||
assert_eq!(get_balance(&db_data, 0).unwrap(), value);
|
||||
|
||||
let extsk2 = ExtendedSpendingKey::master(&[]);
|
||||
let addr2 = extsk2.default_address().unwrap().1;
|
||||
|
@ -785,7 +789,7 @@ mod tests {
|
|||
|
||||
let send_and_recover_with_policy = |ovk_policy| {
|
||||
let tx_row = create_to_address(
|
||||
db_data,
|
||||
&db_data,
|
||||
&network,
|
||||
consensus::BranchId::Blossom,
|
||||
test_prover(),
|
||||
|
@ -797,10 +801,9 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
let data = Connection::open(db_data).unwrap();
|
||||
|
||||
// Fetch the transaction from the database
|
||||
let raw_tx: Vec<_> = data
|
||||
let raw_tx: Vec<_> = db_data
|
||||
.0
|
||||
.query_row(
|
||||
"SELECT raw FROM transactions
|
||||
WHERE id_tx = ?",
|
||||
|
@ -811,7 +814,8 @@ mod tests {
|
|||
let tx = Transaction::read(&raw_tx[..]).unwrap();
|
||||
|
||||
// Fetch the output index from the database
|
||||
let output_index: i64 = data
|
||||
let output_index: i64 = db_data
|
||||
.0
|
||||
.query_row(
|
||||
"SELECT output_index FROM sent_notes
|
||||
WHERE tx = ?",
|
||||
|
@ -847,9 +851,9 @@ mod tests {
|
|||
ExtendedFullViewingKey::from(&ExtendedSpendingKey::master(&[i as u8])),
|
||||
value,
|
||||
);
|
||||
insert_into_cache(db_cache, &cb);
|
||||
insert_into_cache(&db_cache, &cb);
|
||||
}
|
||||
scan_cached_blocks(&network, db_cache, db_data, None).unwrap();
|
||||
scan_cached_blocks(&network, &db_cache, &db_data, None).unwrap();
|
||||
|
||||
// Send the funds again, discarding history.
|
||||
// Neither transaction output is decryptable by the sender.
|
||||
|
|
Loading…
Reference in New Issue