X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning-block-sync%2Fsrc%2Fpoll.rs;h=9f7e8becf5060c2f62471825a2aa0cfeca573f5e;hb=14ee173593e6ee5501e10767ffa6ce1533a9bb2e;hp=82398a4b39cd3ea97f3a77135ebe43b6b94f60c4;hpb=7d6fce78025e7e55a2256a6c16ad4a4dddfcdceb;p=rust-lightning diff --git a/lightning-block-sync/src/poll.rs b/lightning-block-sync/src/poll.rs index 82398a4b..9f7e8bec 100644 --- a/lightning-block-sync/src/poll.rs +++ b/lightning-block-sync/src/poll.rs @@ -1,27 +1,36 @@ -use crate::{AsyncBlockSourceResult, BlockHeaderData, BlockSourceError, BlockSourceResult}; +//! Adapters that make one or more [`BlockSource`]s simpler to poll for new chain tip transitions. + +use crate::{AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, BlockSourceError, BlockSourceResult}; -use bitcoin::blockdata::block::Block; use bitcoin::hash_types::BlockHash; use bitcoin::network::constants::Network; +use lightning::chain::BestBlock; + +use std::ops::Deref; /// The `Poll` trait defines behavior for polling block sources for a chain tip and retrieving /// related chain data. It serves as an adapter for `BlockSource`. +/// +/// [`ChainPoller`] adapts a single `BlockSource`, while any other implementations of `Poll` are +/// required to be built in terms of it to ensure chain data validity. +/// +/// [`ChainPoller`]: ../struct.ChainPoller.html pub trait Poll { /// Returns a chain tip in terms of its relationship to the provided chain tip. - fn poll_chain_tip<'a>(&'a mut self, best_known_chain_tip: ValidatedBlockHeader) -> + fn poll_chain_tip<'a>(&'a self, best_known_chain_tip: ValidatedBlockHeader) -> AsyncBlockSourceResult<'a, ChainTip>; /// Returns the header that preceded the given header in the chain. - fn look_up_previous_header<'a>(&'a mut self, header: &'a ValidatedBlockHeader) -> + fn look_up_previous_header<'a>(&'a self, header: &'a ValidatedBlockHeader) -> AsyncBlockSourceResult<'a, ValidatedBlockHeader>; /// Returns the block associated with the given header. - fn fetch_block<'a>(&'a mut self, header: &'a ValidatedBlockHeader) -> + fn fetch_block<'a>(&'a self, header: &'a ValidatedBlockHeader) -> AsyncBlockSourceResult<'a, ValidatedBlock>; } /// A chain tip relative to another chain tip in terms of block hash and chainwork. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ChainTip { /// A chain tip with the same hash as another chain's tip. Common, @@ -35,7 +44,9 @@ pub enum ChainTip { } /// The `Validate` trait defines behavior for validating chain data. -pub(crate) trait Validate { +/// +/// This trait is sealed and not meant to be implemented outside of this crate. +pub trait Validate: sealed::Validate { /// The validated data wrapper which can be dereferenced to obtain the validated data. type T: std::ops::Deref; @@ -48,12 +59,11 @@ impl Validate for BlockHeaderData { type T = ValidatedBlockHeader; fn validate(self, block_hash: BlockHash) -> BlockSourceResult { - self.header + let pow_valid_block_hash = self.header .validate_pow(&self.header.target()) - .or_else(|e| Err(BlockSourceError::persistent(e)))?; + .map_err(BlockSourceError::persistent)?; - // TODO: Use the result of validate_pow instead of recomputing the block hash once upstream. - if self.header.block_hash() != block_hash { + if pow_valid_block_hash != block_hash { return Err(BlockSourceError::persistent("invalid block hash")); } @@ -61,25 +71,31 @@ impl Validate for BlockHeaderData { } } -impl Validate for Block { +impl Validate for BlockData { type T = ValidatedBlock; fn validate(self, block_hash: BlockHash) -> BlockSourceResult { - self.header - .validate_pow(&self.header.target()) - .or_else(|e| Err(BlockSourceError::persistent(e)))?; + let header = match &self { + BlockData::FullBlock(block) => &block.header, + BlockData::HeaderOnly(header) => header, + }; - // TODO: Use the result of validate_pow instead of recomputing the block hash once upstream. - if self.block_hash() != block_hash { + let pow_valid_block_hash = header + .validate_pow(&header.target()) + .map_err(BlockSourceError::persistent)?; + + if pow_valid_block_hash != block_hash { return Err(BlockSourceError::persistent("invalid block hash")); } - if !self.check_merkle_root() { - return Err(BlockSourceError::persistent("invalid merkle root")); - } + if let BlockData::FullBlock(block) = &self { + if !block.check_merkle_root() { + return Err(BlockSourceError::persistent("invalid merkle root")); + } - if !self.check_witness_commitment() { - return Err(BlockSourceError::persistent("invalid witness commitment")); + if !block.check_witness_commitment() { + return Err(BlockSourceError::persistent("invalid witness commitment")); + } } Ok(ValidatedBlock { block_hash, inner: self }) @@ -87,9 +103,9 @@ impl Validate for Block { } /// A block header with validated proof of work and corresponding block hash. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ValidatedBlockHeader { - block_hash: BlockHash, + pub(crate) block_hash: BlockHash, inner: BlockHeaderData, } @@ -131,18 +147,240 @@ impl ValidatedBlockHeader { Ok(()) } + + /// Returns the [`BestBlock`] corresponding to this validated block header, which can be passed + /// into [`ChannelManager::new`] as part of its [`ChainParameters`]. Useful for ensuring that + /// the [`SpvClient`] and [`ChannelManager`] are initialized to the same block during a fresh + /// start. + /// + /// [`SpvClient`]: crate::SpvClient + /// [`ChainParameters`]: lightning::ln::channelmanager::ChainParameters + /// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager + /// [`ChannelManager::new`]: lightning::ln::channelmanager::ChannelManager::new + pub fn to_best_block(&self) -> BestBlock { + BestBlock::new(self.block_hash, self.inner.height) + } } /// A block with validated data against its transaction list and corresponding block hash. pub struct ValidatedBlock { - block_hash: BlockHash, - inner: Block, + pub(crate) block_hash: BlockHash, + inner: BlockData, } impl std::ops::Deref for ValidatedBlock { - type Target = Block; + type Target = BlockData; fn deref(&self) -> &Self::Target { &self.inner } } + +mod sealed { + /// Used to prevent implementing [`super::Validate`] outside the crate but still allow its use. + pub trait Validate {} + + impl Validate for crate::BlockHeaderData {} + impl Validate for crate::BlockData {} +} + +/// The canonical `Poll` implementation used for a single `BlockSource`. +/// +/// Other `Poll` implementations should be built using `ChainPoller` as it provides the simplest way +/// of validating chain data and checking consistency. +pub struct ChainPoller + Sized + Send + Sync, T: BlockSource + ?Sized> { + block_source: B, + network: Network, +} + +impl + Sized + Send + Sync, T: BlockSource + ?Sized> ChainPoller { + /// Creates a new poller for the given block source. + /// + /// If the `network` parameter is mainnet, then the difficulty between blocks is checked for + /// validity. + pub fn new(block_source: B, network: Network) -> Self { + Self { block_source, network } + } +} + +impl + Sized + Send + Sync, T: BlockSource + ?Sized> Poll for ChainPoller { + fn poll_chain_tip<'a>(&'a self, best_known_chain_tip: ValidatedBlockHeader) -> + AsyncBlockSourceResult<'a, ChainTip> + { + Box::pin(async move { + let (block_hash, height) = self.block_source.get_best_block().await?; + if block_hash == best_known_chain_tip.header.block_hash() { + return Ok(ChainTip::Common); + } + + let chain_tip = self.block_source + .get_header(&block_hash, height).await? + .validate(block_hash)?; + if chain_tip.chainwork > best_known_chain_tip.chainwork { + Ok(ChainTip::Better(chain_tip)) + } else { + Ok(ChainTip::Worse(chain_tip)) + } + }) + } + + fn look_up_previous_header<'a>(&'a self, header: &'a ValidatedBlockHeader) -> + AsyncBlockSourceResult<'a, ValidatedBlockHeader> + { + Box::pin(async move { + if header.height == 0 { + return Err(BlockSourceError::persistent("genesis block reached")); + } + + let previous_hash = &header.header.prev_blockhash; + let height = header.height - 1; + let previous_header = self.block_source + .get_header(previous_hash, Some(height)).await? + .validate(*previous_hash)?; + header.check_builds_on(&previous_header, self.network)?; + + Ok(previous_header) + }) + } + + fn fetch_block<'a>(&'a self, header: &'a ValidatedBlockHeader) -> + AsyncBlockSourceResult<'a, ValidatedBlock> + { + Box::pin(async move { + self.block_source + .get_block(&header.block_hash).await? + .validate(header.block_hash) + }) + } +} + +#[cfg(test)] +mod tests { + use crate::*; + use crate::test_utils::Blockchain; + use super::*; + use bitcoin::util::uint::Uint256; + + #[tokio::test] + async fn poll_empty_chain() { + let mut chain = Blockchain::default().with_height(0); + let best_known_chain_tip = chain.tip(); + chain.disconnect_tip(); + + let poller = ChainPoller::new(&chain, Network::Bitcoin); + match poller.poll_chain_tip(best_known_chain_tip).await { + Err(e) => { + assert_eq!(e.kind(), BlockSourceErrorKind::Transient); + assert_eq!(e.into_inner().as_ref().to_string(), "empty chain"); + }, + Ok(_) => panic!("Expected error"), + } + } + + #[tokio::test] + async fn poll_chain_without_headers() { + let chain = Blockchain::default().with_height(1).without_headers(); + let best_known_chain_tip = chain.at_height(0); + + let poller = ChainPoller::new(&chain, Network::Bitcoin); + match poller.poll_chain_tip(best_known_chain_tip).await { + Err(e) => { + assert_eq!(e.kind(), BlockSourceErrorKind::Persistent); + assert_eq!(e.into_inner().as_ref().to_string(), "header not found"); + }, + Ok(_) => panic!("Expected error"), + } + } + + #[tokio::test] + async fn poll_chain_with_invalid_pow() { + let mut chain = Blockchain::default().with_height(1); + let best_known_chain_tip = chain.at_height(0); + + // Invalidate the tip by changing its target. + chain.blocks.last_mut().unwrap().header.bits = + BlockHeader::compact_target_from_u256(&Uint256::from_be_bytes([0; 32])); + + let poller = ChainPoller::new(&chain, Network::Bitcoin); + match poller.poll_chain_tip(best_known_chain_tip).await { + Err(e) => { + assert_eq!(e.kind(), BlockSourceErrorKind::Persistent); + assert_eq!(e.into_inner().as_ref().to_string(), "block target correct but not attained"); + }, + Ok(_) => panic!("Expected error"), + } + } + + #[tokio::test] + async fn poll_chain_with_malformed_headers() { + let chain = Blockchain::default().with_height(1).malformed_headers(); + let best_known_chain_tip = chain.at_height(0); + + let poller = ChainPoller::new(&chain, Network::Bitcoin); + match poller.poll_chain_tip(best_known_chain_tip).await { + Err(e) => { + assert_eq!(e.kind(), BlockSourceErrorKind::Persistent); + assert_eq!(e.into_inner().as_ref().to_string(), "invalid block hash"); + }, + Ok(_) => panic!("Expected error"), + } + } + + #[tokio::test] + async fn poll_chain_with_common_tip() { + let chain = Blockchain::default().with_height(0); + let best_known_chain_tip = chain.tip(); + + let poller = ChainPoller::new(&chain, Network::Bitcoin); + match poller.poll_chain_tip(best_known_chain_tip).await { + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(tip) => assert_eq!(tip, ChainTip::Common), + } + } + + #[tokio::test] + async fn poll_chain_with_uncommon_tip_but_equal_chainwork() { + let mut chain = Blockchain::default().with_height(1); + let best_known_chain_tip = chain.tip(); + + // Change the nonce to get a different block hash with the same chainwork. + chain.blocks.last_mut().unwrap().header.nonce += 1; + let worse_chain_tip = chain.tip(); + assert_eq!(best_known_chain_tip.chainwork, worse_chain_tip.chainwork); + + let poller = ChainPoller::new(&chain, Network::Bitcoin); + match poller.poll_chain_tip(best_known_chain_tip).await { + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(tip) => assert_eq!(tip, ChainTip::Worse(worse_chain_tip)), + } + } + + #[tokio::test] + async fn poll_chain_with_worse_tip() { + let mut chain = Blockchain::default().with_height(1); + let best_known_chain_tip = chain.tip(); + + chain.disconnect_tip(); + let worse_chain_tip = chain.tip(); + + let poller = ChainPoller::new(&chain, Network::Bitcoin); + match poller.poll_chain_tip(best_known_chain_tip).await { + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(tip) => assert_eq!(tip, ChainTip::Worse(worse_chain_tip)), + } + } + + #[tokio::test] + async fn poll_chain_with_better_tip() { + let chain = Blockchain::default().with_height(1); + let best_known_chain_tip = chain.at_height(0); + + let better_chain_tip = chain.tip(); + + let poller = ChainPoller::new(&chain, Network::Bitcoin); + match poller.poll_chain_tip(best_known_chain_tip).await { + Err(e) => panic!("Unexpected error: {:?}", e), + Ok(tip) => assert_eq!(tip, ChainTip::Better(better_chain_tip)), + } + } +}