diff --git a/zebra-chain/src/types.rs b/zebra-chain/src/types.rs index 2d31c6213..7e7bc79e7 100644 --- a/zebra-chain/src/types.rs +++ b/zebra-chain/src/types.rs @@ -43,7 +43,7 @@ impl fmt::Debug for Sha256dChecksum { /// # Invariants /// /// Users should not construct block heights greater than or equal to `500_000_000`. -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct BlockHeight(pub u32); #[cfg(test)] diff --git a/zebra-consensus/src/checkpoint.rs b/zebra-consensus/src/checkpoint.rs new file mode 100644 index 000000000..d17a6d476 --- /dev/null +++ b/zebra-consensus/src/checkpoint.rs @@ -0,0 +1,158 @@ +//! Checkpoint-based block verification for Zebra. +//! +//! Checkpoint-based verification uses a list of checkpoint hashes to speed up the +//! initial chain sync for Zebra. This list is distributed with Zebra. +//! +//! The CheckpointVerifier compares each block's `BlockHeaderHash` against the known +//! checkpoint hashes. If it matches, then the block is verified, and added to the +//! `ZebraState`. Otherwise, if the block's height is lower than the maximum checkpoint +//! height, the block awaits the verification of its child block. +//! +//! Verification is provided via a `tower::Service`, to support backpressure and batch +//! verification. + +use futures_util::FutureExt; +use std::{ + collections::HashMap, + error, + future::Future, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; +use tower::{Service, ServiceExt}; + +use zebra_chain::block::{Block, BlockHeaderHash}; +use zebra_chain::types::BlockHeight; + +struct CheckpointVerifier { + /// The underlying `ZebraState`. + state_service: S, + + /// Each checkpoint consists of a coinbase height and block header hash. + /// + /// Checkpoints should be chosen to avoid forks or chain reorganizations, + /// which only happen in the last few hundred blocks in the chain. + /// (zcashd allows chain reorganizations up to 99 blocks, and prunes + /// orphaned side-chains after 288 blocks.) + checkpoint_list: Arc>, +} + +/// The error type for the CheckpointVerifier Service. +// TODO(jlusby): Error = Report ? +type Error = Box; + +/// The CheckpointVerifier service implementation. +/// +/// After verification, blocks are added to the underlying state service. +impl Service> for CheckpointVerifier +where + S: Service + + Send + + Clone + + 'static, + S::Future: Send + 'static, +{ + type Response = BlockHeaderHash; + type Error = Error; + type Future = + Pin> + Send + 'static>>; + + fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { + // We don't expect the state to exert backpressure on verifier users, + // so we don't need to call `state_service.poll_ready()` here. + Poll::Ready(Ok(())) + } + + fn call(&mut self, block: Arc) -> Self::Future { + // TODO(jlusby): Error = Report, handle errors from state_service. + let mut state_service = self.state_service.clone(); + let checkpoint_list = self.checkpoint_list.clone(); + + async move { + if checkpoint_list.is_empty() { + return Err("the checkpoint list is empty".into()); + }; + + let block_height = match block.coinbase_height() { + Some(height) => height, + None => return Err("the block does not have a coinbase height".into()), + }; + + // TODO(teor): + // - implement chaining from checkpoints to their ancestors + // - if chaining is expensive, move this check to the Future + // - should the state contain a mapping from previous_block_hash to block? + let checkpoint_hash = match checkpoint_list.get(&block_height) { + Some(&hash) => hash, + None => return Err("the block's height is not a checkpoint height".into()), + }; + + // Hashing is expensive, so we do it as late as possible + if BlockHeaderHash::from(block.as_ref()) != checkpoint_hash { + // The block is on a side-chain + return Err("the block hash does not match the checkpoint hash".into()); + } + + // `Tower::Buffer` requires a 1:1 relationship between `poll()`s + // and `call()`s, because it reserves a buffer slot in each + // `call()`. + // TODO(teor): what happens if the await fails? + let add_block = state_service + .ready_and() + .await? + .call(zebra_state::Request::AddBlock { + block: block.clone(), + }); + + match add_block.await? { + zebra_state::Response::Added { hash } => Ok(hash), + _ => Err("adding block to zebra-state failed".into()), + } + } + .boxed() + } +} + +// TODO(teor): +// - add a function for the maximum checkpoint height +// (We can pre-calculate the result in init(), if we want.) +// - check that block.coinbase_height() <= max_checkpoint_height + +/// Return a checkpoint verification service, using the provided state service. +/// +/// The checkpoint verifier holds a state service of type `S`, into which newly +/// verified blocks will be committed. This state is pluggable to allow for +/// testing or instrumentation. +/// +/// The returned type is opaque to allow instrumentation or other wrappers, but +/// can be boxed for storage. It is also `Clone` to allow sharing of a +/// verification service. +/// +/// This function should be called only once for a particular state service (and +/// the result be shared) rather than constructing multiple verification services +/// backed by the same state layer. +pub fn init( + state_service: S, + checkpoint_list: impl Into>>, +) -> impl Service< + Arc, + Response = BlockHeaderHash, + Error = Error, + Future = impl Future>, +> + Send + + 'static +where + S: Service + + Send + + Clone + + 'static, + S::Future: Send + 'static, +{ + CheckpointVerifier { + state_service, + checkpoint_list: checkpoint_list.into(), + } +} + +// TODO(teor): tests diff --git a/zebra-consensus/src/lib.rs b/zebra-consensus/src/lib.rs index 44c83cf5d..b0d513729 100644 --- a/zebra-consensus/src/lib.rs +++ b/zebra-consensus/src/lib.rs @@ -13,5 +13,6 @@ #![doc(html_root_url = "https://doc.zebra.zfnd.org/zebra_consensus")] #![deny(missing_docs)] +pub mod checkpoint; pub mod mempool; pub mod verify;