zcash_client_sqlite::scan::scan_cached_blocks()

This commit is contained in:
Jack Grigg 2019-03-09 03:12:31 +00:00
parent 68291090c6
commit 9a742d25ea
No known key found for this signature in database
GPG Key ID: 9E8255172BBF9898
5 changed files with 737 additions and 0 deletions

6
Cargo.lock generated
View File

@ -666,6 +666,12 @@ dependencies = [
name = "zcash_client_sqlite"
version = "0.0.0"
dependencies = [
"bech32 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"ff 0.4.0",
"pairing 0.14.2",
"protobuf 2.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_os 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rusqlite 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"zcash_client_backend 0.0.0",

View File

@ -7,9 +7,15 @@ authors = [
edition = "2018"
[dependencies]
bech32 = "0.7"
ff = { path = "../ff" }
protobuf = "2"
rusqlite = { version = "0.20", features = ["bundled"] }
zcash_client_backend = { path = "../zcash_client_backend" }
zcash_primitives = { path = "../zcash_primitives" }
[dev-dependencies]
pairing = { path = "../pairing" }
rand_core = "0.5"
rand_os = "0.2"
tempfile = "3"

View File

@ -1,12 +1,20 @@
use std::error;
use std::fmt;
use zcash_primitives::{sapling::Node, transaction::TxId};
#[derive(Debug)]
pub enum ErrorKind {
CorruptedData(&'static str),
IncorrectHRPExtFVK,
InvalidHeight(i32, i32),
InvalidNewWitnessAnchor(usize, TxId, i32, Node),
InvalidWitnessAnchor(i64, i32),
ScanRequired,
TableNotEmpty,
Bech32(bech32::Error),
Database(rusqlite::Error),
Io(std::io::Error),
Protobuf(protobuf::ProtobufError),
}
#[derive(Debug)]
@ -16,9 +24,28 @@ impl fmt::Display for Error {
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::InvalidHeight(expected, actual) => write!(
f,
"Expected height of next CompactBlock to be {}, but was {}",
expected, actual
),
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::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::Database(e) => write!(f, "{}", e),
ErrorKind::Io(e) => write!(f, "{}", e),
ErrorKind::Protobuf(e) => write!(f, "{}", e),
}
}
}
@ -26,18 +53,39 @@ impl fmt::Display for Error {
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self.0 {
ErrorKind::Bech32(e) => Some(e),
ErrorKind::Database(e) => Some(e),
ErrorKind::Io(e) => Some(e),
ErrorKind::Protobuf(e) => Some(e),
_ => None,
}
}
}
impl From<bech32::Error> for Error {
fn from(e: bech32::Error) -> Self {
Error(ErrorKind::Bech32(e))
}
}
impl From<rusqlite::Error> for Error {
fn from(e: rusqlite::Error) -> Self {
Error(ErrorKind::Database(e))
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error(ErrorKind::Io(e))
}
}
impl From<protobuf::ProtobufError> for Error {
fn from(e: protobuf::ProtobufError) -> Self {
Error(ErrorKind::Protobuf(e))
}
}
impl Error {
pub fn kind(&self) -> &ErrorKind {
&self.0

View File

@ -29,8 +29,10 @@ use zcash_primitives::zip32::ExtendedFullViewingKey;
pub mod error;
pub mod init;
pub mod query;
pub mod scan;
const ANCHOR_OFFSET: u32 = 10;
const SAPLING_ACTIVATION_HEIGHT: i32 = 280_000;
fn address_from_extfvk(extfvk: &ExtendedFullViewingKey) -> String {
let addr = extfvk.default_address().unwrap().1;
@ -63,3 +65,177 @@ fn get_target_and_anchor_heights(data: &Connection) -> Result<(u32, u32), error:
},
)
}
#[cfg(test)]
mod tests {
use ff::{Field, PrimeField, PrimeFieldRepr};
use pairing::bls12_381::Bls12;
use protobuf::Message;
use rand_core::RngCore;
use rand_os::OsRng;
use rusqlite::{types::ToSql, Connection};
use std::path::Path;
use zcash_client_backend::proto::compact_formats::{
CompactBlock, CompactOutput, CompactSpend, CompactTx,
};
use zcash_primitives::{
block::BlockHash,
jubjub::fs::Fs,
note_encryption::{Memo, SaplingNoteEncryption},
primitives::{Note, PaymentAddress},
transaction::components::Amount,
zip32::ExtendedFullViewingKey,
JUBJUB,
};
/// Create a fake CompactBlock at the given height, containing a single output paying
/// the given address. Returns the CompactBlock and the nullifier for the new note.
pub(crate) fn fake_compact_block(
height: i32,
prev_hash: BlockHash,
extfvk: ExtendedFullViewingKey,
value: Amount,
) -> (CompactBlock, Vec<u8>) {
let to = extfvk.default_address().unwrap().1;
// Create a fake Note for the account
let mut rng = OsRng;
let note = Note {
g_d: to.diversifier.g_d::<Bls12>(&JUBJUB).unwrap(),
pk_d: to.pk_d.clone(),
value: value.into(),
r: Fs::random(&mut rng),
};
let encryptor = SaplingNoteEncryption::new(
extfvk.fvk.ovk,
note.clone(),
to.clone(),
Memo::default(),
&mut rng,
);
let mut cmu = vec![];
note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap();
let mut epk = vec![];
encryptor.epk().write(&mut epk).unwrap();
let enc_ciphertext = encryptor.encrypt_note_plaintext();
// Create a fake CompactBlock containing the note
let mut cout = CompactOutput::new();
cout.set_cmu(cmu);
cout.set_epk(epk);
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
let mut ctx = CompactTx::new();
let mut txid = vec![0; 32];
rng.fill_bytes(&mut txid);
ctx.set_hash(txid);
ctx.outputs.push(cout);
let mut cb = CompactBlock::new();
cb.set_height(height as u64);
cb.hash.resize(32, 0);
rng.fill_bytes(&mut cb.hash);
cb.prevHash.extend_from_slice(&prev_hash.0);
cb.vtx.push(ctx);
(cb, note.nf(&extfvk.fvk.vk, 0, &JUBJUB))
}
/// Create a fake CompactBlock at the given height, spending a single note from the
/// given address.
pub(crate) fn fake_compact_block_spending(
height: i32,
prev_hash: BlockHash,
(nf, in_value): (Vec<u8>, Amount),
extfvk: ExtendedFullViewingKey,
to: PaymentAddress<Bls12>,
value: Amount,
) -> CompactBlock {
let mut rng = OsRng;
// Create a fake CompactBlock containing the note
let mut cspend = CompactSpend::new();
cspend.set_nf(nf);
let mut ctx = CompactTx::new();
let mut txid = vec![0; 32];
rng.fill_bytes(&mut txid);
ctx.set_hash(txid);
ctx.spends.push(cspend);
// Create a fake Note for the payment
ctx.outputs.push({
let note = Note {
g_d: to.diversifier.g_d::<Bls12>(&JUBJUB).unwrap(),
pk_d: to.pk_d.clone(),
value: value.into(),
r: Fs::random(&mut rng),
};
let encryptor = SaplingNoteEncryption::new(
extfvk.fvk.ovk,
note.clone(),
to,
Memo::default(),
&mut rng,
);
let mut cmu = vec![];
note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap();
let mut epk = vec![];
encryptor.epk().write(&mut epk).unwrap();
let enc_ciphertext = encryptor.encrypt_note_plaintext();
let mut cout = CompactOutput::new();
cout.set_cmu(cmu);
cout.set_epk(epk);
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
cout
});
// Create a fake Note for the change
ctx.outputs.push({
let change_addr = extfvk.default_address().unwrap().1;
let note = Note {
g_d: change_addr.diversifier.g_d::<Bls12>(&JUBJUB).unwrap(),
pk_d: change_addr.pk_d.clone(),
value: (in_value - value).into(),
r: Fs::random(&mut rng),
};
let encryptor = SaplingNoteEncryption::new(
extfvk.fvk.ovk,
note.clone(),
change_addr,
Memo::default(),
&mut rng,
);
let mut cmu = vec![];
note.cm(&JUBJUB).into_repr().write_le(&mut cmu).unwrap();
let mut epk = vec![];
encryptor.epk().write(&mut epk).unwrap();
let enc_ciphertext = encryptor.encrypt_note_plaintext();
let mut cout = CompactOutput::new();
cout.set_cmu(cmu);
cout.set_epk(epk);
cout.set_ciphertext(enc_ciphertext[..52].to_vec());
cout
});
let mut cb = CompactBlock::new();
cb.set_height(height as u64);
cb.hash.resize(32, 0);
rng.fill_bytes(&mut cb.hash);
cb.prevHash.extend_from_slice(&prev_hash.0);
cb.vtx.push(ctx);
cb
}
/// Insert a fake CompactBlock into the cache DB.
pub(crate) fn insert_into_cache<P: AsRef<Path>>(db_cache: P, cb: &CompactBlock) {
let cb_bytes = cb.write_to_bytes().unwrap();
let cache = Connection::open(&db_cache).unwrap();
cache
.prepare("INSERT INTO compactblocks (height, data) VALUES (?, ?)")
.unwrap()
.execute(&[
(cb.height as i32).to_sql().unwrap(),
cb_bytes.to_sql().unwrap(),
])
.unwrap();
}
}

View File

@ -0,0 +1,501 @@
//! Functions for scanning the chain and extracting relevant information.
use ff::{PrimeField, PrimeFieldRepr};
use protobuf::parse_from_bytes;
use rusqlite::{types::ToSql, Connection, NO_PARAMS};
use std::path::Path;
use zcash_client_backend::{
constants::testnet::HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY,
encoding::decode_extended_full_viewing_key, proto::compact_formats::CompactBlock,
welding_rig::scan_block,
};
use zcash_primitives::{
merkle_tree::{CommitmentTree, IncrementalWitness},
sapling::Node,
JUBJUB,
};
use crate::{
error::{Error, ErrorKind},
SAPLING_ACTIVATION_HEIGHT,
};
struct CompactBlockRow {
height: i32,
data: Vec<u8>,
}
#[derive(Clone)]
struct WitnessRow {
id_note: i64,
witness: IncrementalWitness<Node>,
}
/// Scans new blocks added to the cache for any transactions received by the tracked
/// accounts.
///
/// This function pays attention only to cached blocks with heights greater than the
/// highest scanned block in `db_data`. Cached blocks with lower heights are not verified
/// against previously-scanned blocks. In particular, this function **assumes** that the
/// caller is handling rollbacks.
///
/// For brand-new light client databases, this function starts scanning from the Sapling
/// activation height. This height can be fast-forwarded to a more recent block by calling
/// [`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`].
///
/// # Examples
///
/// ```
/// use zcash_client_sqlite::scan::scan_cached_blocks;
///
/// scan_cached_blocks("/path/to/cache.db", "/path/to/data.db");
/// ```
///
/// [`init_blocks_table`]: crate::init::init_blocks_table
pub fn scan_cached_blocks<P: AsRef<Path>, Q: AsRef<Path>>(
db_cache: P,
db_data: Q,
) -> Result<(), Error> {
let cache = Connection::open(db_cache)?;
let data = Connection::open(db_data)?;
// 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 = data.query_row("SELECT MAX(height) FROM blocks", NO_PARAMS, |row| {
row.get(0).or(Ok(SAPLING_ACTIVATION_HEIGHT - 1))
})?;
// Fetch the CompactBlocks we need to scan
let mut stmt_blocks = cache
.prepare("SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC")?;
let rows = stmt_blocks.query_map(&[last_height], |row| {
Ok(CompactBlockRow {
height: row.get(0)?,
data: row.get(1)?,
})
})?;
// Fetch the ExtendedFullViewingKeys we are tracking
let mut stmt_fetch_accounts =
data.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(HRP_SAPLING_EXTENDED_FULL_VIEWING_KEY, &extfvk)
})
})?;
// 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))?;
// Get the most recent CommitmentTree
let mut stmt_fetch_tree = data.prepare("SELECT sapling_tree FROM blocks WHERE height = ?")?;
let mut tree = stmt_fetch_tree
.query_row(&[last_height], |row| {
row.get(0).map(|data: Vec<_>| {
CommitmentTree::read(&data[..]).unwrap_or_else(|_| CommitmentTree::new())
})
})
.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 witnesses = stmt_fetch_witnesses.query_map(&[last_height], |row| {
let id_note = row.get(0)?;
let data: Vec<_> = row.get(1)?;
Ok(IncrementalWitness::read(&data[..]).map(|witness| WitnessRow { id_note, witness }))
})?;
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 nullifiers = stmt_fetch_nullifiers.query_map(NO_PARAMS, |row| {
let nf: Vec<_> = row.get(1)?;
let account: i64 = row.get(2)?;
Ok((nf, account as usize))
})?;
let mut nullifiers: Vec<_> = nullifiers.collect::<Result<_, _>>()?;
// Prepare per-block SQL statements
let mut stmt_insert_block = data.prepare(
"INSERT INTO blocks (height, hash, time, sapling_tree)
VALUES (?, ?, ?, ?)",
)?;
let mut stmt_update_tx = data.prepare(
"UPDATE transactions
SET block = ?, tx_index = ? WHERE txid = ?",
)?;
let mut stmt_insert_tx = data.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_insert_note = data.prepare(
"INSERT INTO received_notes (tx, output_index, account, diversifier, value, rcm, nf, is_change)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
)?;
let mut stmt_insert_witness = data.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(
"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 < ?
)",
)?;
for row in rows {
let row = row?;
// Start an SQL transaction for this block.
data.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)));
}
last_height = row.height;
let block: CompactBlock = parse_from_bytes(&row.data)?;
let block_hash = block.hash.clone();
let block_time = block.time;
let txs = {
let nf_refs: Vec<_> = nullifiers.iter().map(|(nf, acc)| (&nf[..], *acc)).collect();
let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.witness).collect();
scan_block(
block,
&extfvks[..],
&nf_refs,
&mut tree,
&mut witness_refs[..],
)
};
// Enforce that all roots match. This is slow, so only include in debug builds.
#[cfg(debug_assertions)]
{
let cur_root = tree.root();
for row in &witnesses {
if row.witness.root() != cur_root {
return Err(Error(ErrorKind::InvalidWitnessAnchor(
row.id_note,
last_height,
)));
}
}
for tx in &txs {
for output in tx.shielded_outputs.iter() {
if output.witness.root() != cur_root {
return Err(Error(ErrorKind::InvalidNewWitnessAnchor(
output.index,
tx.txid,
last_height,
output.witness.root(),
)));
}
}
}
}
// Insert the block into the database.
let mut encoded_tree = Vec::new();
tree.write(&mut encoded_tree)
.expect("Should be able to write to a Vec");
stmt_insert_block.execute(&[
row.height.to_sql()?,
block_hash.to_sql()?,
block_time.to_sql()?,
encoded_tree.to_sql()?,
])?;
for tx in txs {
// First try update an existing transaction in the database.
let txid = tx.txid.0.to_vec();
let tx_row = if stmt_update_tx.execute(&[
row.height.to_sql()?,
(tx.index as i64).to_sql()?,
txid.to_sql()?,
])? == 0
{
// It isn't there, so insert our transaction into the database.
stmt_insert_tx.execute(&[
txid.to_sql()?,
row.height.to_sql()?,
(tx.index as i64).to_sql()?,
])?;
data.last_insert_rowid()
} else {
// It was there, so grab its row number.
stmt_select_tx.query_row(&[txid], |row| row.get(0))?
};
// Mark notes as spent and remove them from the scanning cache
for spend in &tx.shielded_spends {
stmt_mark_spent_note.execute(&[tx_row.to_sql()?, spend.nf.to_sql()?])?;
}
nullifiers = nullifiers
.into_iter()
.filter(|(nf, _acc)| {
tx.shielded_spends
.iter()
.find(|spend| &spend.nf == nf)
.is_none()
})
.collect();
for output in tx.shielded_outputs {
let mut rcm = [0; 32];
output.note.r.into_repr().write_le(&mut rcm[..])?;
let nf = output.note.nf(
&extfvks[output.account].fvk.vk,
output.witness.position() as u64,
&JUBJUB,
);
// Insert received note into the database.
// Assumptions:
// - A transaction will not contain more than 2^63 shielded outputs.
// - A note value will never exceed 2^63 zatoshis.
stmt_insert_note.execute(&[
tx_row.to_sql()?,
(output.index as i64).to_sql()?,
(output.account as i64).to_sql()?,
output.to.diversifier.0.to_sql()?,
(output.note.value as i64).to_sql()?,
rcm.to_sql()?,
nf.to_sql()?,
output.is_change.to_sql()?,
])?;
let note_row = data.last_insert_rowid();
// Save witness for note.
witnesses.push(WitnessRow {
id_note: note_row,
witness: output.witness,
});
// Cache nullifier for note (to detect subsequent spends in this scan).
nullifiers.push((nf, output.account));
}
}
// Insert current witnesses into the database.
let mut encoded = Vec::new();
for witness_row in witnesses.iter() {
encoded.clear();
witness_row
.witness
.write(&mut encoded)
.expect("Should be able to write to a Vec");
stmt_insert_witness.execute(&[
witness_row.id_note.to_sql()?,
last_height.to_sql()?,
encoded.to_sql()?,
])?;
}
// Prune the stored witnesses (we only expect rollbacks of at most 100 blocks).
stmt_prune_witnesses.execute(&[last_height - 100])?;
// Update now-expired transactions that didn't get mined.
stmt_update_expired.execute(&[last_height])?;
// Commit the SQL transaction, writing this block's data atomically.
data.execute("COMMIT", NO_PARAMS)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use tempfile::NamedTempFile;
use zcash_primitives::{
block::BlockHash,
transaction::components::Amount,
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
};
use super::scan_cached_blocks;
use crate::{
init::{init_accounts_table, init_cache_database, init_data_database},
query::get_balance,
tests::{fake_compact_block, fake_compact_block_spending, insert_into_cache},
SAPLING_ACTIVATION_HEIGHT,
};
#[test]
fn scan_cached_blocks_requires_sequential_blocks() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = cache_file.path();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = data_file.path();
init_data_database(&db_data).unwrap();
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
// Create a block with height SAPLING_ACTIVATION_HEIGHT
let value = Amount::from_u64(50000).unwrap();
let (cb1, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
BlockHash([0; 32]),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb1);
scan_cached_blocks(db_cache, db_data).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(
SAPLING_ACTIVATION_HEIGHT + 1,
cb1.hash(),
extfvk.clone(),
value,
);
let (cb3, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT + 2,
cb2.hash(),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb3);
match scan_cached_blocks(db_cache, db_data) {
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
)
),
}
// 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(db_cache, db_data).unwrap();
assert_eq!(
get_balance(db_data, 0).unwrap(),
Amount::from_u64(150_000).unwrap()
);
}
#[test]
fn scan_cached_blocks_finds_received_notes() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = cache_file.path();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = data_file.path();
init_data_database(&db_data).unwrap();
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
// Account balance should be 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();
let (cb, _) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
BlockHash([0; 32]),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb);
// Scan the cache
scan_cached_blocks(db_cache, db_data).unwrap();
// Account balance should reflect the received note
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);
// Scan the cache again
scan_cached_blocks(db_cache, db_data).unwrap();
// Account balance should reflect both received notes
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();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = data_file.path();
init_data_database(&db_data).unwrap();
// Add an account to the wallet
let extsk = ExtendedSpendingKey::master(&[]);
let extfvk = ExtendedFullViewingKey::from(&extsk);
init_accounts_table(&db_data, &[extfvk.clone()]).unwrap();
// Account balance should be 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();
let (cb, nf) = fake_compact_block(
SAPLING_ACTIVATION_HEIGHT,
BlockHash([0; 32]),
extfvk.clone(),
value,
);
insert_into_cache(db_cache, &cb);
// Scan the cache
scan_cached_blocks(db_cache, db_data).unwrap();
// Account balance should reflect the received note
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,
&fake_compact_block_spending(
SAPLING_ACTIVATION_HEIGHT + 1,
cb.hash(),
(nf, value),
extfvk,
to2,
value2,
),
);
// Scan the cache again
scan_cached_blocks(db_cache, db_data).unwrap();
// Account balance should equal the change
assert_eq!(get_balance(db_data, 0).unwrap(), value - value2);
}
}