#![allow(clippy::needless_doctest_main)]
use std::convert::Infallible;
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
merkle_tree::CommitmentTree,
sapling::{note_encryption::PreparedIncomingViewingKey, Nullifier},
zip32::Scope,
};
use crate::{
data_api::{PrunedBlock, WalletWrite},
proto::compact_formats::CompactBlock,
scan::BatchRunner,
wallet::WalletTx,
welding_rig::{add_block_to_runner, scan_block_with_runner},
};
pub mod error;
use error::{ChainError, Error};
pub trait BlockSource {
type Error;
fn with_blocks<F, WalletErrT, NoteRefT>(
&self,
from_height: Option<BlockHeight>,
limit: Option<u32>,
with_row: F,
) -> Result<(), error::Error<WalletErrT, Self::Error, NoteRefT>>
where
F: FnMut(CompactBlock) -> Result<(), error::Error<WalletErrT, Self::Error, NoteRefT>>;
}
pub fn validate_chain<BlockSourceT>(
block_source: &BlockSourceT,
mut validate_from: Option<(BlockHeight, BlockHash)>,
limit: Option<u32>,
) -> Result<(), Error<Infallible, BlockSourceT::Error, Infallible>>
where
BlockSourceT: BlockSource,
{
block_source.with_blocks::<_, Infallible, Infallible>(
validate_from.map(|(h, _)| h),
limit,
move |block| {
if let Some((valid_height, valid_hash)) = validate_from {
if block.height() != valid_height + 1 {
return Err(ChainError::block_height_discontinuity(
valid_height + 1,
block.height(),
)
.into());
} else if block.prev_hash() != valid_hash {
return Err(ChainError::prev_hash_mismatch(block.height()).into());
}
}
validate_from = Some((block.height(), block.hash()));
Ok(())
},
)
}
#[tracing::instrument(skip(params, block_source, data_db))]
#[allow(clippy::type_complexity)]
pub fn scan_cached_blocks<ParamsT, DbT, BlockSourceT>(
params: &ParamsT,
block_source: &BlockSourceT,
data_db: &mut DbT,
limit: Option<u32>,
) -> Result<(), Error<DbT::Error, BlockSourceT::Error, DbT::NoteRef>>
where
ParamsT: consensus::Parameters + Send + 'static,
BlockSourceT: BlockSource,
DbT: WalletWrite,
{
let mut last_height = data_db
.block_height_extrema()
.map_err(Error::Wallet)?
.map(|(_, max)| max);
let ufvks = data_db
.get_unified_full_viewing_keys()
.map_err(Error::Wallet)?;
let dfvks: Vec<_> = ufvks
.iter()
.filter_map(|(account, ufvk)| ufvk.sapling().map(move |k| (account, k)))
.collect();
let mut tree = last_height.map_or_else(
|| Ok(CommitmentTree::empty()),
|h| {
data_db
.get_commitment_tree(h)
.map(|t| t.unwrap_or_else(CommitmentTree::empty))
.map_err(Error::Wallet)
},
)?;
let mut witnesses = last_height.map_or_else(
|| Ok(vec![]),
|h| data_db.get_witnesses(h).map_err(Error::Wallet),
)?;
let mut nullifiers = data_db.get_nullifiers().map_err(Error::Wallet)?;
let mut batch_runner = BatchRunner::<_, _, _, ()>::new(
100,
dfvks
.iter()
.flat_map(|(account, dfvk)| {
[
((**account, Scope::External), dfvk.to_ivk(Scope::External)),
((**account, Scope::Internal), dfvk.to_ivk(Scope::Internal)),
]
})
.map(|(tag, ivk)| (tag, PreparedIncomingViewingKey::new(&ivk))),
);
block_source.with_blocks::<_, DbT::Error, DbT::NoteRef>(
last_height,
limit,
|block: CompactBlock| {
add_block_to_runner(params, block, &mut batch_runner);
Ok(())
},
)?;
batch_runner.flush();
block_source.with_blocks::<_, DbT::Error, DbT::NoteRef>(
last_height,
limit,
|block: CompactBlock| {
let current_height = block.height();
if let Some(h) = last_height {
if current_height != (h + 1) {
return Err(
ChainError::block_height_discontinuity(h + 1, current_height).into(),
);
}
}
let block_hash = BlockHash::from_slice(&block.hash);
let block_time = block.time;
let txs: Vec<WalletTx<Nullifier>> = {
let mut witness_refs: Vec<_> = witnesses.iter_mut().map(|w| &mut w.1).collect();
scan_block_with_runner(
params,
block,
&dfvks,
&nullifiers,
&mut tree,
&mut witness_refs[..],
Some(&mut batch_runner),
)
};
#[cfg(debug_assertions)]
{
let cur_root = tree.root();
for row in &witnesses {
if row.1.root() != cur_root {
return Err(
ChainError::invalid_witness_anchor(current_height, row.0).into()
);
}
}
for tx in &txs {
for output in tx.shielded_outputs.iter() {
if output.witness.root() != cur_root {
return Err(ChainError::invalid_new_witness_anchor(
current_height,
tx.txid,
output.index,
output.witness.root(),
)
.into());
}
}
}
}
let new_witnesses = data_db
.advance_by_block(
&(PrunedBlock {
block_height: current_height,
block_hash,
block_time,
commitment_tree: &tree,
transactions: &txs,
}),
&witnesses,
)
.map_err(Error::Wallet)?;
let spent_nf: Vec<Nullifier> = txs
.iter()
.flat_map(|tx| tx.shielded_spends.iter().map(|spend| spend.nf))
.collect();
nullifiers.retain(|(_, nf)| !spent_nf.contains(nf));
nullifiers.extend(
txs.iter()
.flat_map(|tx| tx.shielded_outputs.iter().map(|out| (out.account, out.nf))),
);
witnesses.extend(new_witnesses);
last_height = Some(current_height);
Ok(())
},
)?;
Ok(())
}
#[cfg(feature = "test-dependencies")]
pub mod testing {
use std::convert::Infallible;
use zcash_primitives::consensus::BlockHeight;
use crate::proto::compact_formats::CompactBlock;
use super::{error::Error, BlockSource};
pub struct MockBlockSource;
impl BlockSource for MockBlockSource {
type Error = Infallible;
fn with_blocks<F, DbErrT, NoteRef>(
&self,
_from_height: Option<BlockHeight>,
_limit: Option<u32>,
_with_row: F,
) -> Result<(), Error<DbErrT, Infallible, NoteRef>>
where
F: FnMut(CompactBlock) -> Result<(), Error<DbErrT, Infallible, NoteRef>>,
{
Ok(())
}
}
}