Implement chain validation & fix doctests.

This commit is contained in:
Kris Nuttycombe 2020-08-05 19:14:45 -06:00
parent a437df191e
commit 9874abfd6c
8 changed files with 694 additions and 632 deletions

View File

@ -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(())
}
}

View File

@ -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(&current_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);
}
}

View File

@ -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))
}
}

View File

@ -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

View File

@ -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(&[

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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.