#![deny(rustdoc::broken_intra_doc_links)]
use maybe_rayon::{
prelude::{IndexedParallelIterator, ParallelIterator},
slice::ParallelSliceMut,
};
use rusqlite::{self, Connection};
use secrecy::{ExposeSecret, SecretVec};
use std::{
borrow::Borrow, collections::HashMap, convert::AsRef, fmt, num::NonZeroU32, ops::Range,
path::Path,
};
use incrementalmerkletree::Position;
use shardtree::{
error::{QueryError, ShardTreeError},
ShardTree,
};
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
legacy::TransparentAddress,
memo::{Memo, MemoBytes},
sapling,
transaction::{
components::{amount::Amount, OutPoint},
Transaction, TxId,
},
zip32::{AccountId, DiversifierIndex, ExtendedFullViewingKey},
};
use zcash_client_backend::{
address::{AddressMetadata, UnifiedAddress},
data_api::{
self,
chain::{BlockSource, CommitmentTreeRoot},
scanning::{ScanPriority, ScanRange},
AccountBirthday, BlockMetadata, DecryptedTransaction, NoteId, NullifierQuery, PoolType,
Recipient, ScannedBlock, SentTransaction, ShieldedProtocol, WalletCommitmentTrees,
WalletRead, WalletSummary, WalletWrite, SAPLING_SHARD_HEIGHT,
},
keys::{UnifiedFullViewingKey, UnifiedSpendingKey},
proto::compact_formats::CompactBlock,
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
DecryptedOutput, TransferType,
};
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
#[cfg(feature = "unstable")]
use {
crate::chain::{fsblockdb_with_blocks, BlockMeta},
std::path::PathBuf,
std::{fs, io},
};
pub mod chain;
pub mod error;
pub mod wallet;
use wallet::{
commitment_tree::{self, get_checkpoint_depth, put_shard_roots},
SubtreeScanProgress,
};
#[cfg(test)]
mod testing;
pub(crate) const PRUNING_DEPTH: u32 = 100;
pub(crate) const VERIFY_LOOKAHEAD: u32 = 10;
pub(crate) const SAPLING_TABLES_PREFIX: &str = "sapling";
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ReceivedNoteId(pub(crate) i64);
impl fmt::Display for ReceivedNoteId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ReceivedNoteId(id) => write!(f, "Received Note {}", id),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct UtxoId(pub i64);
pub struct WalletDb<C, P> {
conn: C,
params: P,
}
pub struct SqlTransaction<'conn>(pub(crate) &'conn rusqlite::Transaction<'conn>);
impl Borrow<rusqlite::Connection> for SqlTransaction<'_> {
fn borrow(&self) -> &rusqlite::Connection {
self.0
}
}
impl<P: consensus::Parameters + Clone> WalletDb<Connection, P> {
pub fn for_path<F: AsRef<Path>>(path: F, params: P) -> Result<Self, rusqlite::Error> {
Connection::open(path).and_then(move |conn| {
rusqlite::vtab::array::load_module(&conn)?;
Ok(WalletDb { conn, params })
})
}
pub fn transactionally<F, A, E: From<rusqlite::Error>>(&mut self, f: F) -> Result<A, E>
where
F: FnOnce(&mut WalletDb<SqlTransaction<'_>, P>) -> Result<A, E>,
{
let tx = self.conn.transaction()?;
let mut wdb = WalletDb {
conn: SqlTransaction(&tx),
params: self.params.clone(),
};
let result = f(&mut wdb)?;
tx.commit()?;
Ok(result)
}
}
impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> WalletRead for WalletDb<C, P> {
type Error = SqliteClientError;
type NoteRef = ReceivedNoteId;
fn chain_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::scan_queue_extrema(self.conn.borrow())
.map(|h| h.map(|(_, max)| max))
.map_err(SqliteClientError::from)
}
fn block_metadata(&self, height: BlockHeight) -> Result<Option<BlockMetadata>, Self::Error> {
wallet::block_metadata(self.conn.borrow(), height)
}
fn block_fully_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
wallet::block_fully_scanned(self.conn.borrow())
}
fn block_max_scanned(&self) -> Result<Option<BlockMetadata>, Self::Error> {
wallet::block_max_scanned(self.conn.borrow())
}
fn suggest_scan_ranges(&self) -> Result<Vec<ScanRange>, Self::Error> {
wallet::scanning::suggest_scan_ranges(self.conn.borrow(), ScanPriority::Historic)
.map_err(SqliteClientError::from)
}
fn get_target_and_anchor_heights(
&self,
min_confirmations: NonZeroU32,
) -> Result<Option<(BlockHeight, BlockHeight)>, Self::Error> {
wallet::get_target_and_anchor_heights(self.conn.borrow(), min_confirmations)
.map_err(SqliteClientError::from)
}
fn get_min_unspent_height(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::get_min_unspent_height(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn get_block_hash(&self, block_height: BlockHeight) -> Result<Option<BlockHash>, Self::Error> {
wallet::get_block_hash(self.conn.borrow(), block_height).map_err(SqliteClientError::from)
}
fn get_max_height_hash(&self) -> Result<Option<(BlockHeight, BlockHash)>, Self::Error> {
wallet::get_max_height_hash(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn get_tx_height(&self, txid: TxId) -> Result<Option<BlockHeight>, Self::Error> {
wallet::get_tx_height(self.conn.borrow(), txid).map_err(SqliteClientError::from)
}
fn get_wallet_birthday(&self) -> Result<Option<BlockHeight>, Self::Error> {
wallet::wallet_birthday(self.conn.borrow()).map_err(SqliteClientError::from)
}
fn get_account_birthday(&self, account: AccountId) -> Result<BlockHeight, Self::Error> {
wallet::account_birthday(self.conn.borrow(), account).map_err(SqliteClientError::from)
}
fn get_current_address(
&self,
account: AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
wallet::get_current_address(self.conn.borrow(), &self.params, account)
.map(|res| res.map(|(addr, _)| addr))
}
fn get_unified_full_viewing_keys(
&self,
) -> Result<HashMap<AccountId, UnifiedFullViewingKey>, Self::Error> {
wallet::get_unified_full_viewing_keys(self.conn.borrow(), &self.params)
}
fn get_account_for_ufvk(
&self,
ufvk: &UnifiedFullViewingKey,
) -> Result<Option<AccountId>, Self::Error> {
wallet::get_account_for_ufvk(self.conn.borrow(), &self.params, ufvk)
}
fn is_valid_account_extfvk(
&self,
account: AccountId,
extfvk: &ExtendedFullViewingKey,
) -> Result<bool, Self::Error> {
wallet::is_valid_account_extfvk(self.conn.borrow(), &self.params, account, extfvk)
}
fn get_wallet_summary(
&self,
min_confirmations: u32,
) -> Result<Option<WalletSummary>, Self::Error> {
wallet::get_wallet_summary(self.conn.borrow(), min_confirmations, &SubtreeScanProgress)
}
fn get_memo(&self, note_id: NoteId) -> Result<Option<Memo>, Self::Error> {
let sent_memo = wallet::get_sent_memo(self.conn.borrow(), note_id)?;
if sent_memo.is_some() {
Ok(sent_memo)
} else {
wallet::get_received_memo(self.conn.borrow(), note_id)
}
}
fn get_transaction(&self, txid: TxId) -> Result<Transaction, Self::Error> {
wallet::get_transaction(self.conn.borrow(), &self.params, txid).map(|(_, tx)| tx)
}
fn get_sapling_nullifiers(
&self,
query: NullifierQuery,
) -> Result<Vec<(AccountId, sapling::Nullifier)>, Self::Error> {
match query {
NullifierQuery::Unspent => wallet::sapling::get_sapling_nullifiers(self.conn.borrow()),
NullifierQuery::All => wallet::sapling::get_all_sapling_nullifiers(self.conn.borrow()),
}
}
fn get_spendable_sapling_notes(
&self,
account: AccountId,
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
wallet::sapling::get_spendable_sapling_notes(
self.conn.borrow(),
account,
anchor_height,
exclude,
)
}
fn select_spendable_sapling_notes(
&self,
account: AccountId,
target_value: Amount,
anchor_height: BlockHeight,
exclude: &[Self::NoteRef],
) -> Result<Vec<ReceivedSaplingNote<Self::NoteRef>>, Self::Error> {
wallet::sapling::select_spendable_sapling_notes(
self.conn.borrow(),
account,
target_value,
anchor_height,
exclude,
)
}
fn get_transparent_receivers(
&self,
_account: AccountId,
) -> Result<HashMap<TransparentAddress, AddressMetadata>, Self::Error> {
#[cfg(feature = "transparent-inputs")]
return wallet::get_transparent_receivers(self.conn.borrow(), &self.params, _account);
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
}
fn get_unspent_transparent_outputs(
&self,
_address: &TransparentAddress,
_max_height: BlockHeight,
_exclude: &[OutPoint],
) -> Result<Vec<WalletTransparentOutput>, Self::Error> {
#[cfg(feature = "transparent-inputs")]
return wallet::get_unspent_transparent_outputs(
self.conn.borrow(),
&self.params,
_address,
_max_height,
_exclude,
);
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
}
fn get_transparent_balances(
&self,
_account: AccountId,
_max_height: BlockHeight,
) -> Result<HashMap<TransparentAddress, Amount>, Self::Error> {
#[cfg(feature = "transparent-inputs")]
return wallet::get_transparent_balances(
self.conn.borrow(),
&self.params,
_account,
_max_height,
);
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
}
}
impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P> {
type UtxoRef = UtxoId;
fn create_account(
&mut self,
seed: &SecretVec<u8>,
birthday: AccountBirthday,
) -> Result<(AccountId, UnifiedSpendingKey), Self::Error> {
self.transactionally(|wdb| {
let account = wallet::get_max_account_id(wdb.conn.0)?
.map(|a| AccountId::from(u32::from(a) + 1))
.unwrap_or_else(|| AccountId::from(0));
if u32::from(account) >= 0x7FFFFFFF {
return Err(SqliteClientError::AccountIdOutOfRange);
}
let usk = UnifiedSpendingKey::from_seed(&wdb.params, seed.expose_secret(), account)
.map_err(|_| SqliteClientError::KeyDerivationError(account))?;
let ufvk = usk.to_unified_full_viewing_key();
wallet::add_account(wdb.conn.0, &wdb.params, account, &ufvk, birthday)?;
Ok((account, usk))
})
}
fn get_next_available_address(
&mut self,
account: AccountId,
) -> Result<Option<UnifiedAddress>, Self::Error> {
self.transactionally(
|wdb| match wdb.get_unified_full_viewing_keys()?.get(&account) {
Some(ufvk) => {
let search_from =
match wallet::get_current_address(wdb.conn.0, &wdb.params, account)? {
Some((_, mut last_diversifier_index)) => {
last_diversifier_index
.increment()
.map_err(|_| SqliteClientError::DiversifierIndexOutOfRange)?;
last_diversifier_index
}
None => DiversifierIndex::default(),
};
let (addr, diversifier_index) = ufvk
.find_address(search_from)
.ok_or(SqliteClientError::DiversifierIndexOutOfRange)?;
wallet::insert_address(
wdb.conn.0,
&wdb.params,
account,
diversifier_index,
&addr,
)?;
Ok(Some(addr))
}
None => Ok(None),
},
)
}
#[tracing::instrument(skip_all, fields(height = blocks.first().map(|b| u32::from(b.height()))))]
#[allow(clippy::type_complexity)]
fn put_blocks(
&mut self,
blocks: Vec<ScannedBlock<sapling::Nullifier>>,
) -> Result<(), Self::Error> {
self.transactionally(|wdb| {
let start_positions = blocks.first().map(|block| {
(
block.height(),
Position::from(
u64::from(block.metadata().sapling_tree_size())
- u64::try_from(block.sapling_commitments().len()).unwrap(),
),
)
});
let mut sapling_commitments = vec![];
let mut last_scanned_height = None;
let mut note_positions = vec![];
for block in blocks.into_iter() {
if last_scanned_height
.iter()
.any(|prev| block.height() != *prev + 1)
{
return Err(SqliteClientError::NonSequentialBlocks);
}
wallet::put_block(
wdb.conn.0,
block.height(),
block.block_hash(),
block.block_time(),
block.metadata().sapling_tree_size(),
block.sapling_commitments().len().try_into().unwrap(),
)?;
for tx in block.transactions() {
let tx_row = wallet::put_tx_meta(wdb.conn.0, tx, block.height())?;
for spend in &tx.sapling_spends {
wallet::sapling::mark_sapling_note_spent(wdb.conn.0, tx_row, spend.nf())?;
}
for output in &tx.sapling_outputs {
let spent_in = wallet::query_nullifier_map(
wdb.conn.0,
ShieldedProtocol::Sapling,
output.nf(),
)?;
wallet::sapling::put_received_note(wdb.conn.0, output, tx_row, spent_in)?;
}
}
wallet::insert_nullifier_map(
wdb.conn.0,
block.height(),
ShieldedProtocol::Sapling,
block.sapling_nullifier_map(),
)?;
note_positions.extend(block.transactions().iter().flat_map(|wtx| {
wtx.sapling_outputs
.iter()
.map(|out| out.note_commitment_tree_position())
}));
last_scanned_height = Some(block.height());
sapling_commitments.extend(block.into_sapling_commitments().into_iter().map(Some));
}
if let Some(meta) = wdb.block_fully_scanned()? {
wallet::prune_nullifier_map(
wdb.conn.0,
meta.block_height().saturating_sub(PRUNING_DEPTH),
)?;
}
if let Some(((start_height, start_position), last_scanned_height)) =
start_positions.zip(last_scanned_height)
{
const CHUNK_SIZE: usize = 1024;
let subtrees = sapling_commitments
.par_chunks_mut(CHUNK_SIZE)
.enumerate()
.filter_map(|(i, chunk)| {
let start = start_position + (i * CHUNK_SIZE) as u64;
let end = start + chunk.len() as u64;
shardtree::LocatedTree::from_iter(
start..end,
SAPLING_SHARD_HEIGHT.into(),
chunk.iter_mut().map(|n| n.take().expect("always Some")),
)
})
.map(|res| (res.subtree, res.checkpoints))
.collect::<Vec<_>>();
let mut subtrees = subtrees.into_iter();
wdb.with_sapling_tree_mut::<_, _, SqliteClientError>(move |sapling_tree| {
for (tree, checkpoints) in &mut subtrees {
sapling_tree.insert_tree(tree, checkpoints)?;
}
Ok(())
})?;
wallet::update_expired_notes(wdb.conn.0, last_scanned_height)?;
wallet::scanning::scan_complete(
wdb.conn.0,
&wdb.params,
Range {
start: start_height,
end: last_scanned_height + 1,
},
¬e_positions,
)?;
}
Ok(())
})
}
fn update_chain_tip(&mut self, tip_height: BlockHeight) -> Result<(), Self::Error> {
let tx = self.conn.transaction()?;
wallet::scanning::update_chain_tip(&tx, &self.params, tip_height)?;
tx.commit()?;
Ok(())
}
fn store_decrypted_tx(&mut self, d_tx: DecryptedTransaction) -> Result<(), Self::Error> {
self.transactionally(|wdb| {
let tx_ref = wallet::put_tx_data(wdb.conn.0, d_tx.tx, None, None)?;
let mut spending_account_id: Option<AccountId> = None;
for output in d_tx.sapling_outputs {
match output.transfer_type {
TransferType::Outgoing | TransferType::WalletInternal => {
let recipient = if output.transfer_type == TransferType::Outgoing {
Recipient::Sapling(output.note.recipient())
} else {
Recipient::InternalAccount(
output.account,
PoolType::Shielded(ShieldedProtocol::Sapling)
)
};
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
output.account,
tx_ref,
output.index,
&recipient,
Amount::from_u64(output.note.value().inner()).map_err(|_| {
SqliteClientError::CorruptedData(
"Note value is not a valid Zcash amount.".to_string(),
)
})?,
Some(&output.memo),
)?;
if matches!(recipient, Recipient::InternalAccount(_, _)) {
wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref, None)?;
}
}
TransferType::Incoming => {
match spending_account_id {
Some(id) => {
if id != output.account {
panic!("Unable to determine a unique account identifier for z->t spend.");
}
}
None => {
spending_account_id = Some(output.account);
}
}
wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref, None)?;
}
}
}
#[cfg(feature = "transparent-inputs")]
for txin in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vin.iter()) {
wallet::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, &txin.prevout)?;
}
if d_tx.tx.transparent_bundle().iter().any(|b| !b.vout.is_empty()) {
let nullifiers = wdb.get_sapling_nullifiers(NullifierQuery::All)?;
if let Some((account_id, _)) = nullifiers.iter().find(
|(_, nf)|
d_tx.tx.sapling_bundle().iter().flat_map(|b| b.shielded_spends().iter())
.any(|input| nf == input.nullifier())
) {
for (output_index, txout) in d_tx.tx.transparent_bundle().iter().flat_map(|b| b.vout.iter()).enumerate() {
if let Some(address) = txout.recipient_address() {
wallet::put_sent_output(
wdb.conn.0,
&wdb.params,
*account_id,
tx_ref,
output_index,
&Recipient::Transparent(address),
txout.value,
None
)?;
}
}
}
}
Ok(())
})
}
fn store_sent_tx(&mut self, sent_tx: &SentTransaction) -> Result<(), Self::Error> {
self.transactionally(|wdb| {
let tx_ref = wallet::put_tx_data(
wdb.conn.0,
sent_tx.tx,
Some(sent_tx.fee_amount),
Some(sent_tx.created),
)?;
if let Some(bundle) = sent_tx.tx.sapling_bundle() {
for spend in bundle.shielded_spends() {
wallet::sapling::mark_sapling_note_spent(
wdb.conn.0,
tx_ref,
spend.nullifier(),
)?;
}
}
#[cfg(feature = "transparent-inputs")]
for utxo_outpoint in &sent_tx.utxos_spent {
wallet::mark_transparent_utxo_spent(wdb.conn.0, tx_ref, utxo_outpoint)?;
}
for output in &sent_tx.outputs {
wallet::insert_sent_output(
wdb.conn.0,
&wdb.params,
tx_ref,
sent_tx.account,
output,
)?;
if let Some((account, note)) = output.sapling_change_to() {
wallet::sapling::put_received_note(
wdb.conn.0,
&DecryptedOutput {
index: output.output_index(),
note: note.clone(),
account: *account,
memo: output
.memo()
.map_or_else(MemoBytes::empty, |memo| memo.clone()),
transfer_type: TransferType::WalletInternal,
},
tx_ref,
None,
)?;
}
}
Ok(())
})
}
fn truncate_to_height(&mut self, block_height: BlockHeight) -> Result<(), Self::Error> {
self.transactionally(|wdb| {
wallet::truncate_to_height(wdb.conn.0, &wdb.params, block_height)
})
}
fn put_received_transparent_utxo(
&mut self,
_output: &WalletTransparentOutput,
) -> Result<Self::UtxoRef, Self::Error> {
#[cfg(feature = "transparent-inputs")]
return wallet::put_received_transparent_utxo(&self.conn, &self.params, _output);
#[cfg(not(feature = "transparent-inputs"))]
panic!(
"The wallet must be compiled with the transparent-inputs feature to use this method."
);
}
}
impl<P: consensus::Parameters> WalletCommitmentTrees for WalletDb<rusqlite::Connection, P> {
type Error = commitment_tree::Error;
type SaplingShardStore<'a> =
SqliteShardStore<&'a rusqlite::Transaction<'a>, sapling::Node, SAPLING_SHARD_HEIGHT>;
fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::SaplingShardStore<'a>,
{ sapling::NOTE_COMMITMENT_TREE_DEPTH },
SAPLING_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<Self::Error>>,
{
let tx = self
.conn
.transaction()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
let shard_store = SqliteShardStore::from_connection(&tx, SAPLING_TABLES_PREFIX)
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
let result = {
let mut shardtree = ShardTree::new(shard_store, PRUNING_DEPTH.try_into().unwrap());
callback(&mut shardtree)?
};
tx.commit()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
Ok(result)
}
fn put_sapling_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<sapling::Node>],
) -> Result<(), ShardTreeError<Self::Error>> {
let tx = self
.conn
.transaction()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
&tx,
SAPLING_TABLES_PREFIX,
start_index,
roots,
)?;
tx.commit()
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
Ok(())
}
fn get_checkpoint_depth(
&self,
min_confirmations: NonZeroU32,
) -> Result<usize, ShardTreeError<Self::Error>> {
get_checkpoint_depth(&self.conn, SAPLING_TABLES_PREFIX, min_confirmations)
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?
.ok_or(ShardTreeError::Query(QueryError::CheckpointPruned))
}
}
impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTransaction<'conn>, P> {
type Error = commitment_tree::Error;
type SaplingShardStore<'a> =
SqliteShardStore<&'a rusqlite::Transaction<'a>, sapling::Node, SAPLING_SHARD_HEIGHT>;
fn with_sapling_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
where
for<'a> F: FnMut(
&'a mut ShardTree<
Self::SaplingShardStore<'a>,
{ sapling::NOTE_COMMITMENT_TREE_DEPTH },
SAPLING_SHARD_HEIGHT,
>,
) -> Result<A, E>,
E: From<ShardTreeError<commitment_tree::Error>>,
{
let mut shardtree = ShardTree::new(
SqliteShardStore::from_connection(self.conn.0, SAPLING_TABLES_PREFIX)
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?,
PRUNING_DEPTH.try_into().unwrap(),
);
let result = callback(&mut shardtree)?;
Ok(result)
}
fn put_sapling_subtree_roots(
&mut self,
start_index: u64,
roots: &[CommitmentTreeRoot<sapling::Node>],
) -> Result<(), ShardTreeError<Self::Error>> {
put_shard_roots::<_, { sapling::NOTE_COMMITMENT_TREE_DEPTH }, SAPLING_SHARD_HEIGHT>(
self.conn.0,
SAPLING_TABLES_PREFIX,
start_index,
roots,
)
}
fn get_checkpoint_depth(
&self,
min_confirmations: NonZeroU32,
) -> Result<usize, ShardTreeError<Self::Error>> {
get_checkpoint_depth(self.conn.0, SAPLING_TABLES_PREFIX, min_confirmations)
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?
.ok_or(ShardTreeError::Query(QueryError::CheckpointPruned))
}
}
pub struct BlockDb(Connection);
impl BlockDb {
pub fn for_path<P: AsRef<Path>>(path: P) -> Result<Self, rusqlite::Error> {
Connection::open(path).map(BlockDb)
}
}
impl BlockSource for BlockDb {
type Error = SqliteClientError;
fn with_blocks<F, DbErrT>(
&self,
from_height: Option<BlockHeight>,
limit: Option<usize>,
with_row: F,
) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
where
F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
{
chain::blockdb_with_blocks(self, from_height, limit, with_row)
}
}
#[cfg(feature = "unstable")]
pub struct FsBlockDb {
conn: Connection,
blocks_dir: PathBuf,
}
#[derive(Debug)]
#[cfg(feature = "unstable")]
pub enum FsBlockDbError {
Fs(io::Error),
Db(rusqlite::Error),
Protobuf(prost::DecodeError),
MissingBlockPath(PathBuf),
InvalidBlockstoreRoot(PathBuf),
InvalidBlockPath(PathBuf),
CorruptedData(String),
CacheMiss(BlockHeight),
}
#[cfg(feature = "unstable")]
impl From<io::Error> for FsBlockDbError {
fn from(err: io::Error) -> Self {
FsBlockDbError::Fs(err)
}
}
#[cfg(feature = "unstable")]
impl From<rusqlite::Error> for FsBlockDbError {
fn from(err: rusqlite::Error) -> Self {
FsBlockDbError::Db(err)
}
}
#[cfg(feature = "unstable")]
impl From<prost::DecodeError> for FsBlockDbError {
fn from(e: prost::DecodeError) -> Self {
FsBlockDbError::Protobuf(e)
}
}
#[cfg(feature = "unstable")]
impl FsBlockDb {
pub fn for_path<P: AsRef<Path>>(fsblockdb_root: P) -> Result<Self, FsBlockDbError> {
let meta = fs::metadata(&fsblockdb_root).map_err(FsBlockDbError::Fs)?;
if meta.is_dir() {
let db_path = fsblockdb_root.as_ref().join("blockmeta.sqlite");
let blocks_dir = fsblockdb_root.as_ref().join("blocks");
fs::create_dir_all(&blocks_dir)?;
Ok(FsBlockDb {
conn: Connection::open(db_path).map_err(FsBlockDbError::Db)?,
blocks_dir,
})
} else {
Err(FsBlockDbError::InvalidBlockstoreRoot(
fsblockdb_root.as_ref().to_path_buf(),
))
}
}
pub fn get_max_cached_height(&self) -> Result<Option<BlockHeight>, FsBlockDbError> {
Ok(chain::blockmetadb_get_max_cached_height(&self.conn)?)
}
pub fn write_block_metadata(&self, block_meta: &[BlockMeta]) -> Result<(), FsBlockDbError> {
for m in block_meta {
let block_path = m.block_file_path(&self.blocks_dir);
match fs::metadata(&block_path) {
Err(e) => {
return Err(match e.kind() {
io::ErrorKind::NotFound => FsBlockDbError::MissingBlockPath(block_path),
_ => FsBlockDbError::Fs(e),
});
}
Ok(meta) => {
if !meta.is_file() {
return Err(FsBlockDbError::InvalidBlockPath(block_path));
}
}
}
}
Ok(chain::blockmetadb_insert(&self.conn, block_meta)?)
}
pub fn find_block(&self, height: BlockHeight) -> Result<Option<BlockMeta>, FsBlockDbError> {
Ok(chain::blockmetadb_find_block(&self.conn, height)?)
}
pub fn truncate_to_height(&self, block_height: BlockHeight) -> Result<(), FsBlockDbError> {
Ok(chain::blockmetadb_truncate_to_height(
&self.conn,
block_height,
)?)
}
}
#[cfg(feature = "unstable")]
impl BlockSource for FsBlockDb {
type Error = FsBlockDbError;
fn with_blocks<F, DbErrT>(
&self,
from_height: Option<BlockHeight>,
limit: Option<usize>,
with_row: F,
) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>
where
F: FnMut(CompactBlock) -> Result<(), data_api::chain::error::Error<DbErrT, Self::Error>>,
{
fsblockdb_with_blocks(self, from_height, limit, with_row)
}
}
#[cfg(feature = "unstable")]
impl std::fmt::Display for FsBlockDbError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
FsBlockDbError::Fs(io_error) => {
write!(f, "Failed to access the file system: {}", io_error)
}
FsBlockDbError::Db(e) => {
write!(f, "There was a problem with the sqlite db: {}", e)
}
FsBlockDbError::Protobuf(e) => {
write!(f, "Failed to parse protobuf-encoded record: {}", e)
}
FsBlockDbError::MissingBlockPath(block_path) => {
write!(
f,
"CompactBlock file expected but not found at {}",
block_path.display(),
)
}
FsBlockDbError::InvalidBlockstoreRoot(fsblockdb_root) => {
write!(
f,
"The block storage root {} is not a directory",
fsblockdb_root.display(),
)
}
FsBlockDbError::InvalidBlockPath(block_path) => {
write!(
f,
"CompactBlock path {} is not a file",
block_path.display(),
)
}
FsBlockDbError::CorruptedData(e) => {
write!(
f,
"The block cache has corrupted data and this caused an error: {}",
e,
)
}
FsBlockDbError::CacheMiss(height) => {
write!(
f,
"Requested height {} does not exist in the block cache",
height
)
}
}
}
}
#[cfg(test)]
#[macro_use]
extern crate assert_matches;
#[cfg(test)]
mod tests {
use zcash_client_backend::data_api::{AccountBirthday, WalletRead, WalletWrite};
use crate::{testing::TestBuilder, AccountId};
#[cfg(feature = "unstable")]
use {
crate::testing::AddressType,
zcash_client_backend::keys::sapling,
zcash_primitives::{consensus::Parameters, transaction::components::Amount},
};
#[test]
pub(crate) fn get_next_available_address() {
let mut st = TestBuilder::new()
.with_test_account(AccountBirthday::from_sapling_activation)
.build();
let account = AccountId::from(0);
let current_addr = st.wallet().get_current_address(account).unwrap();
assert!(current_addr.is_some());
let addr2 = st.wallet_mut().get_next_available_address(account).unwrap();
assert!(addr2.is_some());
assert_ne!(current_addr, addr2);
let addr2_cur = st.wallet().get_current_address(account).unwrap();
assert_eq!(addr2, addr2_cur);
}
#[cfg(feature = "transparent-inputs")]
#[test]
fn transparent_receivers() {
let st = TestBuilder::new()
.with_block_cache()
.with_test_account(AccountBirthday::from_sapling_activation)
.build();
let (_, usk, _) = st.test_account().unwrap();
let ufvk = usk.to_unified_full_viewing_key();
let (taddr, _) = usk.default_transparent_address();
let receivers = st.wallet().get_transparent_receivers(0.into()).unwrap();
assert!(receivers.contains_key(ufvk.default_address().0.transparent().unwrap()));
assert!(receivers.contains_key(&taddr));
}
#[cfg(feature = "unstable")]
#[test]
pub(crate) fn fsblockdb_api() {
let mut st = TestBuilder::new().with_fs_block_cache().build();
assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
let seed = [0u8; 32];
let account = AccountId::from(0);
let extsk = sapling::spending_key(&seed, st.wallet().params.coin_type(), account);
let dfvk = extsk.to_diversifiable_full_viewing_key();
let (h1, meta1, _) = st.generate_next_block(
&dfvk,
AddressType::DefaultExternal,
Amount::from_u64(5).unwrap(),
);
let (h2, meta2, _) = st.generate_next_block(
&dfvk,
AddressType::DefaultExternal,
Amount::from_u64(10).unwrap(),
);
assert_eq!(st.cache().get_max_cached_height().unwrap(), None);
st.cache().write_block_metadata(&[meta1, meta2]).unwrap();
assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h2),);
assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1));
assert_eq!(st.cache().find_block(h2).unwrap(), Some(meta2));
assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
st.cache().truncate_to_height(h1).unwrap();
assert_eq!(st.cache().get_max_cached_height().unwrap(), Some(h1));
assert_eq!(st.cache().find_block(h1).unwrap(), Some(meta1));
assert_eq!(st.cache().find_block(h2).unwrap(), None);
assert_eq!(st.cache().find_block(h2 + 1).unwrap(), None);
}
}