X-Git-Url: http://git.bitcoin.ninja/index.cgi?a=blobdiff_plain;f=lightning-block-sync%2Fsrc%2Fpoll.rs;h=3ff7606b8d9cfdd8db1f9a936c14d4624c9e8945;hb=05ec06129c00655523780b2ea6ce57a538456164;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..3ff7606b 100644 --- a/lightning-block-sync/src/poll.rs +++ b/lightning-block-sync/src/poll.rs @@ -1,11 +1,18 @@ -use crate::{AsyncBlockSourceResult, BlockHeaderData, BlockSourceError, BlockSourceResult}; +use crate::{AsyncBlockSourceResult, BlockHeaderData, BlockSource, BlockSourceError, BlockSourceResult}; use bitcoin::blockdata::block::Block; use bitcoin::hash_types::BlockHash; use bitcoin::network::constants::Network; +use std::ops::DerefMut; + /// 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) -> @@ -146,3 +153,204 @@ impl std::ops::Deref for ValidatedBlock { &self.inner } } + +/// The canonical `Poll` implementation used for a single `BlockSource`. +/// +/// Other `Poll` implementations must be built using `ChainPoller` as it provides the only means of +/// validating chain data. +pub struct ChainPoller + Sized + Sync + Send, T: BlockSource> { + block_source: B, + network: Network, +} + +impl + Sized + Sync + Send, T: BlockSource> 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 + Sync + Send, T: BlockSource> Poll for ChainPoller { + fn poll_chain_tip<'a>(&'a mut 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 mut 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 mut 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 mut poller = ChainPoller::new(&mut 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 mut chain = Blockchain::default().with_height(1).without_headers(); + let best_known_chain_tip = chain.at_height(0); + + let mut poller = ChainPoller::new(&mut 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 mut poller = ChainPoller::new(&mut 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 mut chain = Blockchain::default().with_height(1).malformed_headers(); + let best_known_chain_tip = chain.at_height(0); + + let mut poller = ChainPoller::new(&mut 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 mut chain = Blockchain::default().with_height(0); + let best_known_chain_tip = chain.tip(); + + let mut poller = ChainPoller::new(&mut 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 mut poller = ChainPoller::new(&mut 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 mut poller = ChainPoller::new(&mut 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 mut chain = Blockchain::default().with_height(1); + let best_known_chain_tip = chain.at_height(0); + + let better_chain_tip = chain.tip(); + + let mut poller = ChainPoller::new(&mut 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)), + } + } +}