312 lines
11 KiB
Rust
312 lines
11 KiB
Rust
#![allow(clippy::needless_doctest_main)]
|
|
//! Tools for blockchain validation & scanning
|
|
//!
|
|
//! # Examples
|
|
//!
|
|
//! ```
|
|
//! # #[cfg(feature = "test-dependencies")]
|
|
//! # {
|
|
//! use zcash_primitives::{
|
|
//! consensus::{BlockHeight, Network, Parameters}
|
|
//! };
|
|
//!
|
|
//! use zcash_client_backend::{
|
|
//! data_api::{
|
|
//! BlockSource, WalletRead, WalletWrite,
|
|
//! chain::{
|
|
//! validate_chain,
|
|
//! scan_cached_blocks,
|
|
//! },
|
|
//! error::Error,
|
|
//! testing,
|
|
//! },
|
|
//! };
|
|
//!
|
|
//! # fn main() {
|
|
//! # test();
|
|
//! # }
|
|
//! #
|
|
//! # fn test() -> Result<(), Error<u32>> {
|
|
//! let network = Network::TestNetwork;
|
|
//! let db_cache = testing::MockBlockSource {};
|
|
//! let mut db_data = testing::MockWalletDb {};
|
|
//!
|
|
//! // 1) Download new CompactBlocks into db_cache.
|
|
//!
|
|
//! // 2) Run the chain validator on the received blocks.
|
|
//! //
|
|
//! // Given that we assume the server always gives us correct-at-the-time blocks, any
|
|
//! // errors are in the blocks we have previously cached or scanned.
|
|
//! if let Err(e) = validate_chain(&network, &db_cache, db_data.get_max_height_hash()?) {
|
|
//! match e {
|
|
//! Error::InvalidChain(lower_bound, _) => {
|
|
//! // a) Pick a height to rewind to.
|
|
//! //
|
|
//! // This might be informed by some external chain reorg information, or
|
|
//! // heuristics such as the platform, available bandwidth, size of recent
|
|
//! // CompactBlocks, etc.
|
|
//! let rewind_height = lower_bound - 10;
|
|
//!
|
|
//! // b) Rewind scanned block information.
|
|
//! db_data.rewind_to_height(rewind_height);
|
|
//!
|
|
//! // c) Delete cached blocks from rewind_height onwards.
|
|
//! //
|
|
//! // This does imply that assumed-valid blocks will be re-downloaded, but it
|
|
//! // is also possible that in the intervening time, a chain reorg has
|
|
//! // occurred that orphaned some of those blocks.
|
|
//!
|
|
//! // d) If there is some separate thread or service downloading
|
|
//! // CompactBlocks, tell it to go back and download from rewind_height
|
|
//! // onwards.
|
|
//! },
|
|
//! e => {
|
|
//! // handle or return other errors
|
|
//!
|
|
//! }
|
|
//! }
|
|
//! }
|
|
//!
|
|
//! // 3) Scan (any remaining) cached blocks.
|
|
//! //
|
|
//! // At this point, the cache and scanned data are locally consistent (though not
|
|
//! // necessarily consistent with the latest chain tip - this would be discovered the
|
|
//! // next time this codepath is executed after new blocks are received).
|
|
//! scan_cached_blocks(&network, &db_cache, &mut db_data, None)
|
|
//! # }
|
|
//! # }
|
|
//! ```
|
|
|
|
use std::fmt::Debug;
|
|
|
|
use zcash_primitives::{
|
|
block::BlockHash,
|
|
consensus::{self, BlockHeight, NetworkUpgrade},
|
|
merkle_tree::CommitmentTree,
|
|
sapling::Nullifier,
|
|
};
|
|
|
|
use crate::{
|
|
data_api::{
|
|
error::{ChainInvalid, Error},
|
|
BlockSource, PrunedBlock, WalletWrite,
|
|
},
|
|
proto::compact_formats::CompactBlock,
|
|
wallet::WalletTx,
|
|
welding_rig::scan_block,
|
|
};
|
|
|
|
/// Checks that the scanned blocks in the data database, when combined with the recent
|
|
/// `CompactBlock`s in the cache database, form a valid chain.
|
|
///
|
|
/// This function is built on the core assumption that the information provided in the
|
|
/// cache database is more likely to be accurate than the previously-scanned information.
|
|
/// This follows from the design (and trust) assumption that the `lightwalletd` server
|
|
/// provides accurate block information as of the time it was requested.
|
|
///
|
|
/// Arguments:
|
|
/// - `parameters` Network parameters
|
|
/// - `cache` Source of compact blocks
|
|
/// - `from_tip` Height & hash of last validated block; if no validation has previously
|
|
/// been performed, this will begin scanning from `sapling_activation_height - 1`
|
|
///
|
|
/// Returns:
|
|
/// - `Ok(())` if the combined chain is valid.
|
|
/// - `Err(ErrorKind::InvalidChain(upper_bound, cause))` if the combined chain is invalid.
|
|
/// `upper_bound` is the height of the highest invalid block (on the assumption that the
|
|
/// highest block in the cache database is correct).
|
|
/// - `Err(e)` if there was an error during validation unrelated to chain validity.
|
|
///
|
|
/// This function does not mutate either of the databases.
|
|
pub fn validate_chain<N, E, P, C>(
|
|
parameters: &P,
|
|
cache: &C,
|
|
validate_from: Option<(BlockHeight, BlockHash)>,
|
|
) -> Result<(), E>
|
|
where
|
|
E: From<Error<N>>,
|
|
P: consensus::Parameters,
|
|
C: BlockSource<Error = E>,
|
|
{
|
|
let sapling_activation_height = parameters
|
|
.activation_height(NetworkUpgrade::Sapling)
|
|
.ok_or(Error::SaplingNotActive)?;
|
|
|
|
// The cache will contain blocks above the `validate_from` height. Validate from that maximum
|
|
// height up to the chain tip, returning the hash of the block found in the cache at the
|
|
// `validate_from` height, which can then be used to verify chain integrity by comparing
|
|
// against the `validate_from` hash.
|
|
let from_height = validate_from
|
|
.map(|(height, _)| height)
|
|
.unwrap_or(sapling_activation_height - 1);
|
|
|
|
let mut prev_height = from_height;
|
|
let mut prev_hash: Option<BlockHash> = validate_from.map(|(_, hash)| hash);
|
|
|
|
cache.with_blocks(from_height, None, move |block| {
|
|
let current_height = block.height();
|
|
let result = if current_height != prev_height + 1 {
|
|
Err(ChainInvalid::block_height_discontinuity(
|
|
prev_height + 1,
|
|
current_height,
|
|
))
|
|
} else {
|
|
match prev_hash {
|
|
None => Ok(()),
|
|
Some(h) if h == block.prev_hash() => Ok(()),
|
|
Some(_) => Err(ChainInvalid::prev_hash_mismatch(current_height)),
|
|
}
|
|
};
|
|
|
|
prev_height = current_height;
|
|
prev_hash = Some(block.hash());
|
|
result.map_err(E::from)
|
|
})
|
|
}
|
|
|
|
#[allow(clippy::needless_doctest_main)]
|
|
/// Scans at most `limit` new blocks added to the cache for any transactions received by
|
|
/// the tracked accounts.
|
|
///
|
|
/// This function will return without error after scanning at most `limit` new blocks, to
|
|
/// enable the caller to update their UI with scanning progress. Repeatedly calling this
|
|
/// function will process sequential ranges of blocks, and is equivalent to calling
|
|
/// `scan_cached_blocks` and passing `None` for the optional `limit` value.
|
|
///
|
|
/// This function pays attention only to cached blocks with heights greater than the
|
|
/// highest scanned block in `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
|
|
/// initializing the client database with a starting block (for example, calling
|
|
/// `init_blocks_table` before this function if using `zcash_client_sqlite`).
|
|
///
|
|
/// Scanned blocks are required to be height-sequential. If a block is missing from the
|
|
/// cache, an error will be returned with kind [`ChainInvalid::BlockHeightDiscontinuity`].
|
|
pub fn scan_cached_blocks<E, N, P, C, D>(
|
|
params: &P,
|
|
cache: &C,
|
|
data: &mut D,
|
|
limit: Option<u32>,
|
|
) -> Result<(), E>
|
|
where
|
|
P: consensus::Parameters,
|
|
C: BlockSource<Error = E>,
|
|
D: WalletWrite<Error = E, NoteRef = N>,
|
|
N: Copy + Debug,
|
|
E: From<Error<N>>,
|
|
{
|
|
let sapling_activation_height = params
|
|
.activation_height(NetworkUpgrade::Sapling)
|
|
.ok_or(Error::SaplingNotActive)?;
|
|
|
|
// 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.block_height_extrema().map(|opt| {
|
|
opt.map(|(_, max)| max)
|
|
.unwrap_or(sapling_activation_height - 1)
|
|
})?;
|
|
|
|
// Fetch the UnifiedFullViewingKeys we are tracking
|
|
let ufvks = data.get_unified_full_viewing_keys()?;
|
|
// TODO: Change `scan_block` to also scan Orchard.
|
|
// https://github.com/zcash/librustzcash/issues/403
|
|
let dfvks: Vec<_> = ufvks
|
|
.iter()
|
|
.filter_map(|(account, ufvk)| ufvk.sapling().map(move |k| (account, k)))
|
|
.collect();
|
|
|
|
// Get the most recent CommitmentTree
|
|
let mut tree = data
|
|
.get_commitment_tree(last_height)
|
|
.map(|t| t.unwrap_or_else(CommitmentTree::empty))?;
|
|
|
|
// Get most recent incremental witnesses for the notes we are tracking
|
|
let mut witnesses = data.get_witnesses(last_height)?;
|
|
|
|
// Get the nullifiers for the notes we are tracking
|
|
let mut nullifiers = data.get_nullifiers()?;
|
|
|
|
cache.with_blocks(last_height, limit, |block: CompactBlock| {
|
|
let current_height = block.height();
|
|
|
|
// Scanned blocks MUST be height-sequential.
|
|
if current_height != (last_height + 1) {
|
|
return Err(
|
|
ChainInvalid::block_height_discontinuity(last_height + 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(
|
|
params,
|
|
block,
|
|
&dfvks,
|
|
&nullifiers,
|
|
&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.1.root() != cur_root {
|
|
return Err(Error::InvalidWitnessAnchor(row.0, current_height).into());
|
|
}
|
|
}
|
|
for tx in &txs {
|
|
for output in tx.shielded_outputs.iter() {
|
|
if output.witness.root() != cur_root {
|
|
return Err(Error::InvalidNewWitnessAnchor(
|
|
output.index,
|
|
tx.txid,
|
|
current_height,
|
|
output.witness.root(),
|
|
)
|
|
.into());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let new_witnesses = data.advance_by_block(
|
|
&(PrunedBlock {
|
|
block_height: current_height,
|
|
block_hash,
|
|
block_time,
|
|
commitment_tree: &tree,
|
|
transactions: &txs,
|
|
}),
|
|
&witnesses,
|
|
)?;
|
|
|
|
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 = current_height;
|
|
|
|
Ok(())
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|