#![allow(clippy::needless_doctest_main)]
use std::ops::Range;
use incrementalmerkletree::frontier::Frontier;
use subtle::ConditionallySelectable;
use zcash_primitives::block::BlockHash;
use zcash_protocol::consensus::{self, BlockHeight};
use crate::{
data_api::WalletWrite,
proto::compact_formats::CompactBlock,
scanning::{
Nullifiers, ScanningKeys,
compact::{BatchRunners, scan_block_with_runners},
},
};
#[cfg(feature = "sync")]
use {
super::scanning::ScanPriority, crate::data_api::scanning::ScanRange, async_trait::async_trait,
};
pub mod error;
use error::Error;
use super::WalletRead;
#[derive(Debug)]
pub struct CommitmentTreeRoot<H> {
subtree_end_height: BlockHeight,
root_hash: H,
}
impl<H> CommitmentTreeRoot<H> {
pub fn from_parts(subtree_end_height: BlockHeight, root_hash: H) -> Self {
Self {
subtree_end_height,
root_hash,
}
}
pub fn subtree_end_height(&self) -> BlockHeight {
self.subtree_end_height
}
pub fn root_hash(&self) -> &H {
&self.root_hash
}
}
pub trait BlockSource {
type Error;
fn with_blocks<F, WalletErrT>(
&self,
from_height: Option<BlockHeight>,
limit: Option<usize>,
with_block: F,
) -> Result<(), error::Error<WalletErrT, Self::Error>>
where
F: FnMut(CompactBlock) -> Result<(), error::Error<WalletErrT, Self::Error>>;
}
#[cfg(feature = "sync")]
#[async_trait]
pub trait BlockCache: BlockSource + Send + Sync
where
Self::Error: Send,
{
fn get_tip_height(&self, range: Option<&ScanRange>)
-> Result<Option<BlockHeight>, Self::Error>;
async fn read(&self, range: &ScanRange) -> Result<Vec<CompactBlock>, Self::Error>;
async fn insert(&self, compact_blocks: Vec<CompactBlock>) -> Result<(), Self::Error>;
async fn truncate(&self, block_height: BlockHeight) -> Result<(), Self::Error> {
if let Some(latest) = self.get_tip_height(None)? {
self.delete(ScanRange::from_parts(
Range {
start: block_height + 1,
end: latest + 1,
},
ScanPriority::Ignored,
))
.await?;
}
Ok(())
}
async fn delete(&self, range: ScanRange) -> Result<(), Self::Error>;
}
#[derive(Clone, Debug)]
pub struct ScanSummary {
pub(crate) scanned_range: Range<BlockHeight>,
pub(crate) spent_sapling_note_count: usize,
pub(crate) received_sapling_note_count: usize,
#[cfg(feature = "orchard")]
pub(crate) spent_orchard_note_count: usize,
#[cfg(feature = "orchard")]
pub(crate) received_orchard_note_count: usize,
}
impl ScanSummary {
pub(crate) fn for_range(scanned_range: Range<BlockHeight>) -> Self {
Self {
scanned_range,
spent_sapling_note_count: 0,
received_sapling_note_count: 0,
#[cfg(feature = "orchard")]
spent_orchard_note_count: 0,
#[cfg(feature = "orchard")]
received_orchard_note_count: 0,
}
}
pub fn scanned_range(&self) -> Range<BlockHeight> {
self.scanned_range.clone()
}
pub fn spent_sapling_note_count(&self) -> usize {
self.spent_sapling_note_count
}
pub fn received_sapling_note_count(&self) -> usize {
self.received_sapling_note_count
}
#[cfg(feature = "orchard")]
pub fn spent_orchard_note_count(&self) -> usize {
self.spent_orchard_note_count
}
#[cfg(feature = "orchard")]
pub fn received_orchard_note_count(&self) -> usize {
self.received_orchard_note_count
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainState {
block_height: BlockHeight,
block_hash: BlockHash,
final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
#[cfg(feature = "orchard")]
final_orchard_tree:
Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>,
#[cfg(feature = "orchard")]
final_ironwood_tree:
Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>,
}
impl ChainState {
pub fn empty(block_height: BlockHeight, block_hash: BlockHash) -> Self {
Self {
block_height,
block_hash,
final_sapling_tree: Frontier::empty(),
#[cfg(feature = "orchard")]
final_orchard_tree: Frontier::empty(),
#[cfg(feature = "orchard")]
final_ironwood_tree: Frontier::empty(),
}
}
pub fn new(
block_height: BlockHeight,
block_hash: BlockHash,
final_sapling_tree: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
#[cfg(feature = "orchard")] final_orchard_tree: Frontier<
orchard::tree::MerkleHashOrchard,
{ orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
>,
#[cfg(feature = "orchard")] final_ironwood_tree: Frontier<
orchard::tree::MerkleHashOrchard,
{ orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 },
>,
) -> Self {
Self {
block_height,
block_hash,
final_sapling_tree,
#[cfg(feature = "orchard")]
final_orchard_tree,
#[cfg(feature = "orchard")]
final_ironwood_tree,
}
}
pub fn block_height(&self) -> BlockHeight {
self.block_height
}
pub fn block_hash(&self) -> BlockHash {
self.block_hash
}
pub fn final_sapling_tree(
&self,
) -> &Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }> {
&self.final_sapling_tree
}
#[cfg(feature = "orchard")]
pub fn final_orchard_tree(
&self,
) -> &Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>
{
&self.final_orchard_tree
}
#[cfg(feature = "orchard")]
pub fn final_ironwood_tree(
&self,
) -> &Frontier<orchard::tree::MerkleHashOrchard, { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 }>
{
&self.final_ironwood_tree
}
}
#[tracing::instrument(skip(params, block_source, data_db, from_state))]
#[allow(clippy::type_complexity)]
pub fn scan_cached_blocks<ParamsT, DbT, BlockSourceT>(
params: &ParamsT,
block_source: &BlockSourceT,
data_db: &mut DbT,
from_height: BlockHeight,
from_state: &ChainState,
limit: usize,
) -> Result<ScanSummary, Error<<DbT as WalletRead>::Error, BlockSourceT::Error>>
where
ParamsT: consensus::Parameters + Send + 'static,
BlockSourceT: BlockSource,
DbT: WalletWrite,
<DbT as WalletRead>::AccountId: ConditionallySelectable + Default + Send + Sync + 'static,
{
assert_eq!(from_height, from_state.block_height + 1);
let account_ufvks = data_db
.get_unified_full_viewing_keys()
.map_err(Error::Wallet)?;
let scanning_keys = ScanningKeys::from_account_ufvks(account_ufvks);
let mut runners = BatchRunners::<_, (), (), ()>::for_keys(100, &scanning_keys);
block_source.with_blocks::<_, <DbT as WalletRead>::Error>(
Some(from_height),
Some(limit),
|block| runners.add_block(params, block).map_err(|e| e.into()),
)?;
runners.flush();
let mut prior_block_metadata = if from_height > BlockHeight::from(0) {
data_db
.block_metadata(from_height - 1)
.map_err(Error::Wallet)?
} else {
None
};
let mut nullifiers = Nullifiers::unspent(data_db).map_err(Error::Wallet)?;
let mut scanned_blocks = vec![];
let mut scan_summary = ScanSummary::for_range(from_height..from_height);
block_source.with_blocks::<_, <DbT as WalletRead>::Error>(
Some(from_height),
Some(limit),
|block: CompactBlock| {
scan_summary.scanned_range.end = block.height() + 1;
let scanned_block = scan_block_with_runners::<_, _, _, (), (), ()>(
params,
block,
&scanning_keys,
&nullifiers,
prior_block_metadata.as_ref(),
Some(&mut runners),
)
.map_err(Error::Scan)?;
for wtx in &scanned_block.transactions {
scan_summary.spent_sapling_note_count += wtx.sapling_spends().len();
scan_summary.received_sapling_note_count += wtx.sapling_outputs().len();
#[cfg(feature = "orchard")]
{
scan_summary.spent_orchard_note_count += wtx.orchard_spends().len();
scan_summary.received_orchard_note_count += wtx.orchard_outputs().len();
}
}
nullifiers.update_with(&scanned_block);
prior_block_metadata = Some(scanned_block.to_block_metadata());
scanned_blocks.push(scanned_block);
Ok(())
},
)?;
data_db
.put_blocks(from_state, scanned_blocks)
.map_err(Error::Wallet)?;
Ok(scan_summary)
}
#[cfg(feature = "test-dependencies")]
pub mod testing {
use std::convert::Infallible;
use zcash_protocol::consensus::BlockHeight;
use crate::proto::compact_formats::CompactBlock;
use super::{BlockSource, error::Error};
pub struct MockBlockSource;
impl BlockSource for MockBlockSource {
type Error = Infallible;
fn with_blocks<F, DbErrT>(
&self,
_from_height: Option<BlockHeight>,
_limit: Option<usize>,
_with_row: F,
) -> Result<(), Error<DbErrT, Infallible>>
where
F: FnMut(CompactBlock) -> Result<(), Error<DbErrT, Infallible>>,
{
Ok(())
}
}
}