librustzcash/zcash_client_sqlite/src/chain.rs

550 lines
18 KiB
Rust

//! Functions for enforcing chain validity and handling chain reorgs.
use protobuf::Message;
use rusqlite::params;
use zcash_primitives::consensus::BlockHeight;
use zcash_client_backend::{data_api::error::Error, proto::compact_formats::CompactBlock};
use crate::{error::SqliteClientError, BlockDb};
pub mod init;
struct CompactBlockRow {
height: BlockHeight,
data: Vec<u8>,
}
/// Implements a traversal of `limit` blocks of the block cache database.
///
/// Starting at `from_height`, the `with_row` callback is invoked
/// with each block retrieved from the backing store. If the `limit`
/// value provided is `None`, all blocks are traversed up to the
/// maximum height.
pub fn with_blocks<F>(
cache: &BlockDb,
from_height: BlockHeight,
limit: Option<u32>,
mut with_row: F,
) -> Result<(), SqliteClientError>
where
F: FnMut(CompactBlock) -> Result<(), SqliteClientError>,
{
// Fetch the CompactBlocks we need to scan
let mut stmt_blocks = cache.0.prepare(
"SELECT height, data FROM compactblocks WHERE height > ? ORDER BY height ASC LIMIT ?",
)?;
let rows = stmt_blocks.query_map(
params![u32::from(from_height), limit.unwrap_or(u32::max_value()),],
|row| {
Ok(CompactBlockRow {
height: BlockHeight::from_u32(row.get(0)?),
data: row.get(1)?,
})
},
)?;
for row_result in rows {
let cbr = row_result?;
let block: CompactBlock = Message::parse_from_bytes(&cbr.data).map_err(Error::from)?;
if block.height() != cbr.height {
return Err(SqliteClientError::CorruptedData(format!(
"Block height {} did not match row's height field value {}",
block.height(),
cbr.height
)));
}
with_row(block)?;
}
Ok(())
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use tempfile::NamedTempFile;
use zcash_primitives::{
block::BlockHash, transaction::components::Amount, zip32::ExtendedSpendingKey,
};
use zcash_client_backend::data_api::WalletRead;
use zcash_client_backend::data_api::{
chain::{scan_cached_blocks, validate_chain},
error::{ChainInvalid, Error},
};
use crate::{
chain::init::init_cache_database,
error::SqliteClientError,
tests::{
self, fake_compact_block, fake_compact_block_spending, init_test_accounts_table,
insert_into_cache, sapling_activation_height,
},
wallet::{get_balance, init::init_wallet_db, rewind_to_height},
AccountId, BlockDb, NoteId, WalletDb,
};
#[test]
fn valid_chain_states() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = BlockDb::for_path(cache_file.path()).unwrap();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
// Empty chain should be valid
validate_chain(
&tests::network(),
&db_cache,
(&db_data).get_max_height_hash().unwrap(),
)
.unwrap();
// Create a fake CompactBlock sending value to the address
let (cb, _) = fake_compact_block(
sapling_activation_height(),
BlockHash([0; 32]),
&dfvk,
Amount::from_u64(5).unwrap(),
);
insert_into_cache(&db_cache, &cb);
// Cache-only chain should be valid
validate_chain(
&tests::network(),
&db_cache,
(&db_data).get_max_height_hash().unwrap(),
)
.unwrap();
// Scan the cache
let mut db_write = db_data.get_update_ops().unwrap();
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Data-only chain should be valid
validate_chain(
&tests::network(),
&db_cache,
(&db_data).get_max_height_hash().unwrap(),
)
.unwrap();
// Create a second fake CompactBlock sending more value to the address
let (cb2, _) = fake_compact_block(
sapling_activation_height() + 1,
cb.hash(),
&dfvk,
Amount::from_u64(7).unwrap(),
);
insert_into_cache(&db_cache, &cb2);
// Data+cache chain should be valid
validate_chain(
&tests::network(),
&db_cache,
(&db_data).get_max_height_hash().unwrap(),
)
.unwrap();
// Scan the cache again
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Data-only chain should be valid
validate_chain(
&tests::network(),
&db_cache,
(&db_data).get_max_height_hash().unwrap(),
)
.unwrap();
}
#[test]
fn invalid_chain_cache_disconnected() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = BlockDb::for_path(cache_file.path()).unwrap();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
// Create some fake CompactBlocks
let (cb, _) = fake_compact_block(
sapling_activation_height(),
BlockHash([0; 32]),
&dfvk,
Amount::from_u64(5).unwrap(),
);
let (cb2, _) = fake_compact_block(
sapling_activation_height() + 1,
cb.hash(),
&dfvk,
Amount::from_u64(7).unwrap(),
);
insert_into_cache(&db_cache, &cb);
insert_into_cache(&db_cache, &cb2);
// Scan the cache
let mut db_write = db_data.get_update_ops().unwrap();
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Data-only chain should be valid
validate_chain(
&tests::network(),
&db_cache,
(&db_data).get_max_height_hash().unwrap(),
)
.unwrap();
// Create more fake CompactBlocks that don't connect to the scanned ones
let (cb3, _) = fake_compact_block(
sapling_activation_height() + 2,
BlockHash([1; 32]),
&dfvk,
Amount::from_u64(8).unwrap(),
);
let (cb4, _) = fake_compact_block(
sapling_activation_height() + 3,
cb3.hash(),
&dfvk,
Amount::from_u64(3).unwrap(),
);
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_chain(
&tests::network(),
&db_cache,
(&db_data).get_max_height_hash().unwrap(),
) {
Err(SqliteClientError::BackendError(Error::InvalidChain(lower_bound, _))) => {
assert_eq!(lower_bound, sapling_activation_height() + 2)
}
_ => panic!(),
}
}
#[test]
fn invalid_chain_cache_reorg() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = BlockDb::for_path(cache_file.path()).unwrap();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
// Create some fake CompactBlocks
let (cb, _) = fake_compact_block(
sapling_activation_height(),
BlockHash([0; 32]),
&dfvk,
Amount::from_u64(5).unwrap(),
);
let (cb2, _) = fake_compact_block(
sapling_activation_height() + 1,
cb.hash(),
&dfvk,
Amount::from_u64(7).unwrap(),
);
insert_into_cache(&db_cache, &cb);
insert_into_cache(&db_cache, &cb2);
// Scan the cache
let mut db_write = db_data.get_update_ops().unwrap();
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Data-only chain should be valid
validate_chain(
&tests::network(),
&db_cache,
(&db_data).get_max_height_hash().unwrap(),
)
.unwrap();
// Create more fake CompactBlocks that contain a reorg
let (cb3, _) = fake_compact_block(
sapling_activation_height() + 2,
cb2.hash(),
&dfvk,
Amount::from_u64(8).unwrap(),
);
let (cb4, _) = fake_compact_block(
sapling_activation_height() + 3,
BlockHash([1; 32]),
&dfvk,
Amount::from_u64(3).unwrap(),
);
insert_into_cache(&db_cache, &cb3);
insert_into_cache(&db_cache, &cb4);
// Data+cache chain should be invalid inside the cache
match validate_chain(
&tests::network(),
&db_cache,
(&db_data).get_max_height_hash().unwrap(),
) {
Err(SqliteClientError::BackendError(Error::InvalidChain(lower_bound, _))) => {
assert_eq!(lower_bound, sapling_activation_height() + 3)
}
_ => panic!(),
}
}
#[test]
fn data_db_rewinding() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = BlockDb::for_path(cache_file.path()).unwrap();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
// Account balance should be zero
assert_eq!(
get_balance(&db_data, AccountId::from(0)).unwrap(),
Amount::zero()
);
// Create fake CompactBlocks sending value to the address
let value = Amount::from_u64(5).unwrap();
let value2 = Amount::from_u64(7).unwrap();
let (cb, _) = fake_compact_block(
sapling_activation_height(),
BlockHash([0; 32]),
&dfvk,
value,
);
let (cb2, _) =
fake_compact_block(sapling_activation_height() + 1, cb.hash(), &dfvk, value2);
insert_into_cache(&db_cache, &cb);
insert_into_cache(&db_cache, &cb2);
// Scan the cache
let mut db_write = db_data.get_update_ops().unwrap();
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Account balance should reflect both received notes
assert_eq!(
get_balance(&db_data, AccountId::from(0)).unwrap(),
(value + value2).unwrap()
);
// "Rewind" to height of last scanned block
rewind_to_height(&db_data, sapling_activation_height() + 1).unwrap();
// Account balance should be unaltered
assert_eq!(
get_balance(&db_data, AccountId::from(0)).unwrap(),
(value + value2).unwrap()
);
// Rewind so that one block is dropped
rewind_to_height(&db_data, sapling_activation_height()).unwrap();
// Account balance should only contain the first received note
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
// Scan the cache again
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Account balance should again reflect both received notes
assert_eq!(
get_balance(&db_data, AccountId::from(0)).unwrap(),
(value + value2).unwrap()
);
}
#[test]
fn scan_cached_blocks_requires_sequential_blocks() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = BlockDb::for_path(cache_file.path()).unwrap();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
// 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]),
&dfvk,
value,
);
insert_into_cache(&db_cache, &cb1);
let mut db_write = db_data.get_update_ops().unwrap();
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
assert_eq!(get_balance(&db_data, AccountId::from(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(), &dfvk, value);
let (cb3, _) =
fake_compact_block(sapling_activation_height() + 2, cb2.hash(), &dfvk, value);
insert_into_cache(&db_cache, &cb3);
match scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None) {
Err(SqliteClientError::BackendError(e)) => {
assert_eq!(
e.to_string(),
ChainInvalid::block_height_discontinuity::<NoteId>(
sapling_activation_height() + 1,
sapling_activation_height() + 2
)
.to_string()
);
}
Ok(_) | Err(_) => panic!("Should have failed"),
}
// 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, &mut db_write, None).unwrap();
assert_eq!(
get_balance(&db_data, AccountId::from(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 = BlockDb::for_path(cache_file.path()).unwrap();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
// Account balance should be zero
assert_eq!(
get_balance(&db_data, AccountId::from(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]),
&dfvk,
value,
);
insert_into_cache(&db_cache, &cb);
// Scan the cache
let mut db_write = db_data.get_update_ops().unwrap();
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Account balance should reflect the received note
assert_eq!(get_balance(&db_data, AccountId::from(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(), &dfvk, value2);
insert_into_cache(&db_cache, &cb2);
// Scan the cache again
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Account balance should reflect both received notes
assert_eq!(
get_balance(&db_data, AccountId::from(0)).unwrap(),
(value + value2).unwrap()
);
}
#[test]
fn scan_cached_blocks_finds_change_notes() {
let cache_file = NamedTempFile::new().unwrap();
let db_cache = BlockDb::for_path(cache_file.path()).unwrap();
init_cache_database(&db_cache).unwrap();
let data_file = NamedTempFile::new().unwrap();
let db_data = WalletDb::for_path(data_file.path(), tests::network()).unwrap();
init_wallet_db(&db_data).unwrap();
// Add an account to the wallet
let (dfvk, _taddr) = init_test_accounts_table(&db_data);
// Account balance should be zero
assert_eq!(
get_balance(&db_data, AccountId::from(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]),
&dfvk,
value,
);
insert_into_cache(&db_cache, &cb);
// Scan the cache
let mut db_write = db_data.get_update_ops().unwrap();
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Account balance should reflect the received note
assert_eq!(get_balance(&db_data, AccountId::from(0)).unwrap(), value);
// Create a second fake CompactBlock spending value from the address
let extsk2 = ExtendedSpendingKey::master(&[0]);
let to2 = extsk2.default_address().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),
&dfvk,
to2,
value2,
),
);
// Scan the cache again
scan_cached_blocks(&tests::network(), &db_cache, &mut db_write, None).unwrap();
// Account balance should equal the change
assert_eq!(
get_balance(&db_data, AccountId::from(0)).unwrap(),
(value - value2).unwrap()
);
}
}