forked from ZcashFoundation/zebra
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
consensus: Add a checkpoint verifier stub (ZcashFoundation#502)
consensus: Add a checkpoint verifier stub This stub only verifies blocks whose hashes are in the checkpoint list. It doesn't have tests, chain child verifies to their ancestors, or support checkpoint maximum height queries. Part of ZcashFoundation#429.
- Loading branch information
Showing
3 changed files
with
160 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<S> { | ||
/// 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<HashMap<BlockHeight, BlockHeaderHash>>, | ||
} | ||
|
||
/// The error type for the CheckpointVerifier Service. | ||
// TODO(jlusby): Error = Report ? | ||
type Error = Box<dyn error::Error + Send + Sync + 'static>; | ||
|
||
/// The CheckpointVerifier service implementation. | ||
/// | ||
/// After verification, blocks are added to the underlying state service. | ||
impl<S> Service<Arc<Block>> for CheckpointVerifier<S> | ||
where | ||
S: Service<zebra_state::Request, Response = zebra_state::Response, Error = Error> | ||
+ Send | ||
+ Clone | ||
+ 'static, | ||
S::Future: Send + 'static, | ||
{ | ||
type Response = BlockHeaderHash; | ||
type Error = Error; | ||
type Future = | ||
Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>; | ||
|
||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { | ||
// 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<Block>) -> 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<S>( | ||
state_service: S, | ||
checkpoint_list: impl Into<Arc<HashMap<BlockHeight, BlockHeaderHash>>>, | ||
) -> impl Service< | ||
Arc<Block>, | ||
Response = BlockHeaderHash, | ||
Error = Error, | ||
Future = impl Future<Output = Result<BlockHeaderHash, Error>>, | ||
> + Send | ||
+ 'static | ||
where | ||
S: Service<zebra_state::Request, Response = zebra_state::Response, Error = Error> | ||
+ Send | ||
+ Clone | ||
+ 'static, | ||
S::Future: Send + 'static, | ||
{ | ||
CheckpointVerifier { | ||
state_service, | ||
checkpoint_list: checkpoint_list.into(), | ||
} | ||
} | ||
|
||
// TODO(teor): tests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters