Skip to main content

lightning_block_sync/
lib.rs

1//! A lightweight client for keeping in sync with chain activity.
2//!
3//! Defines an [`SpvClient`] utility for polling one or more block sources for the best chain tip.
4//! It is used to notify listeners of blocks connected or disconnected since the last poll. Useful
5//! for keeping a Lightning node in sync with the chain.
6//!
7//! Defines a [`BlockSource`] trait, which is an asynchronous interface for retrieving block headers
8//! and data.
9//!
10//! Enabling feature `rest-client` or `rpc-client` allows configuring the client to fetch blocks
11//! using Bitcoin Core's REST or RPC interface, respectively.
12//!
13//! Both features support either blocking I/O using `std::net::TcpStream` or, with feature `tokio`,
14//! non-blocking I/O using `tokio::net::TcpStream` from inside a Tokio runtime.
15
16#![deny(rustdoc::broken_intra_doc_links)]
17#![deny(rustdoc::private_intra_doc_links)]
18#![deny(missing_docs)]
19#![cfg_attr(docsrs, feature(doc_cfg))]
20
21extern crate alloc;
22extern crate core;
23
24#[cfg(any(feature = "rest-client", feature = "rpc-client"))]
25pub mod http;
26
27pub mod init;
28pub mod poll;
29
30pub mod gossip;
31
32#[cfg(feature = "rest-client")]
33pub mod rest;
34
35#[cfg(feature = "rpc-client")]
36pub mod rpc;
37
38#[cfg(any(feature = "rest-client", feature = "rpc-client"))]
39mod convert;
40
41#[cfg(test)]
42mod test_utils;
43
44#[cfg(any(feature = "rest-client", feature = "rpc-client"))]
45mod utils;
46
47#[allow(unused)]
48mod async_poll;
49
50use crate::poll::{ChainTip, Poll, ValidatedBlockHeader};
51
52use bitcoin::block::{Block, Header};
53use bitcoin::hash_types::BlockHash;
54use bitcoin::pow::Work;
55
56use lightning::chain;
57use lightning::chain::BlockLocator;
58
59use std::future::Future;
60use std::ops::Deref;
61
62/// Abstract type for retrieving block headers and data.
63pub trait BlockSource: Sync + Send {
64	/// Returns the header for a given hash. A height hint may be provided in case a block source
65	/// cannot easily find headers based on a hash. This is merely a hint and thus the returned
66	/// header must have the same hash as was requested. Otherwise, an error must be returned.
67	///
68	/// Implementations that cannot find headers based on the hash should return a `Transient` error
69	/// when `height_hint` is `None`.
70	fn get_header<'a>(
71		&'a self, header_hash: &'a BlockHash, height_hint: Option<u32>,
72	) -> impl Future<Output = BlockSourceResult<BlockHeaderData>> + Send + 'a;
73
74	/// Returns the block for a given hash. A headers-only block source should return a `Transient`
75	/// error.
76	fn get_block<'a>(
77		&'a self, header_hash: &'a BlockHash,
78	) -> impl Future<Output = BlockSourceResult<BlockData>> + Send + 'a;
79
80	/// Returns the hash of the best block and, optionally, its height.
81	///
82	/// When polling a block source, [`Poll`] implementations may pass the height to [`get_header`]
83	/// to allow for a more efficient lookup.
84	///
85	/// [`get_header`]: Self::get_header
86	fn get_best_block<'a>(
87		&'a self,
88	) -> impl Future<Output = BlockSourceResult<(BlockHash, Option<u32>)>> + Send + 'a;
89}
90
91/// Result type for `BlockSource` requests.
92pub type BlockSourceResult<T> = Result<T, BlockSourceError>;
93
94/// Error type for `BlockSource` requests.
95///
96/// Transient errors may be resolved when re-polling, but no attempt will be made to re-poll on
97/// persistent errors.
98#[derive(Debug)]
99pub struct BlockSourceError {
100	kind: BlockSourceErrorKind,
101	error: Box<dyn std::error::Error + Send + Sync>,
102}
103
104/// The kind of `BlockSourceError`, either persistent or transient.
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum BlockSourceErrorKind {
107	/// Indicates an error that won't resolve when retrying a request (e.g., invalid data).
108	Persistent,
109
110	/// Indicates an error that may resolve when retrying a request (e.g., unresponsive).
111	Transient,
112}
113
114impl BlockSourceError {
115	/// Creates a new persistent error originated from the given error.
116	pub fn persistent<E>(error: E) -> Self
117	where
118		E: Into<Box<dyn std::error::Error + Send + Sync>>,
119	{
120		Self { kind: BlockSourceErrorKind::Persistent, error: error.into() }
121	}
122
123	/// Creates a new transient error originated from the given error.
124	pub fn transient<E>(error: E) -> Self
125	where
126		E: Into<Box<dyn std::error::Error + Send + Sync>>,
127	{
128		Self { kind: BlockSourceErrorKind::Transient, error: error.into() }
129	}
130
131	/// Returns the kind of error.
132	pub fn kind(&self) -> BlockSourceErrorKind {
133		self.kind
134	}
135
136	/// Converts the error into the underlying error.
137	///
138	/// May contain an [`std::io::Error`] from the [`BlockSource`]. See implementations for further
139	/// details, if any.
140	pub fn into_inner(self) -> Box<dyn std::error::Error + Send + Sync> {
141		self.error
142	}
143}
144
145/// A block header and some associated data. This information should be available from most block
146/// sources (and, notably, is available in Bitcoin Core's RPC and REST interfaces).
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub struct BlockHeaderData {
149	/// The block header itself.
150	pub header: Header,
151
152	/// The block height where the genesis block has height 0.
153	pub height: u32,
154
155	/// The total chain work required to build a chain of equivalent weight.
156	pub chainwork: Work,
157}
158
159/// A block including either all its transactions or only the block header.
160///
161/// [`BlockSource`] may be implemented to either always return full blocks or, in the case of
162/// compact block filters (BIP 157/158), return header-only blocks when no pertinent transactions
163/// match. See [`chain::Filter`] for details on how to notify a source of such transactions.
164pub enum BlockData {
165	/// A block containing all its transactions.
166	FullBlock(Block),
167	/// A block header for when the block does not contain any pertinent transactions.
168	HeaderOnly(Header),
169}
170
171/// A lightweight client for keeping a listener in sync with the chain, allowing for Simplified
172/// Payment Verification (SPV).
173///
174/// The client is parameterized by a chain poller which is responsible for polling one or more block
175/// sources for the best chain tip. During this process it detects any chain forks, determines which
176/// constitutes the best chain, and updates the listener accordingly with any blocks that were
177/// connected or disconnected since the last poll.
178pub struct SpvClient<P: Poll, L: Deref>
179where
180	L::Target: chain::Listen,
181{
182	chain_tip: ValidatedBlockHeader,
183	chain_poller: P,
184	header_cache: HeaderCache,
185	chain_listener: L,
186}
187
188/// The maximum number of [`ValidatedBlockHeader`]s stored in a [`HeaderCache`].
189pub const HEADER_CACHE_LIMIT: u32 = 6 * 24 * 7;
190
191/// Bounded cache of block headers keyed by block hash.
192///
193/// Retains only the latest [`HEADER_CACHE_LIMIT`] block headers based on height.
194pub struct HeaderCache {
195	headers: std::collections::HashMap<BlockHash, ValidatedBlockHeader>,
196	/// When set, [`Self::blocks_disconnected`] will not evict headers above the fork point.
197	/// This is used during initial sync to retain headers across multiple listeners.
198	retain_on_disconnect: bool,
199}
200
201impl HeaderCache {
202	/// Creates a new empty header cache.
203	pub fn new() -> Self {
204		Self { headers: std::collections::HashMap::new(), retain_on_disconnect: false }
205	}
206
207	/// Retrieves the block header keyed by the given block hash.
208	pub fn look_up(&self, block_hash: &BlockHash) -> Option<&ValidatedBlockHeader> {
209		self.headers.get(block_hash)
210	}
211
212	/// Called when a block has been connected to the best chain to ensure it is available to be
213	/// disconnected later if needed.
214	pub(crate) fn block_connected(
215		&mut self, block_hash: BlockHash, block_header: ValidatedBlockHeader,
216	) {
217		self.headers.insert(block_hash, block_header);
218
219		// Remove headers older than a week.
220		let cutoff_height = block_header.height.saturating_sub(HEADER_CACHE_LIMIT);
221		self.headers.retain(|_, header| header.height >= cutoff_height);
222	}
223
224	/// Inserts the given block header during a find_difference operation, implying it might not be
225	/// the best header.
226	pub(crate) fn insert_during_diff(
227		&mut self, block_hash: BlockHash, block_header: ValidatedBlockHeader,
228	) {
229		self.headers.insert(block_hash, block_header);
230
231		// Remove headers older than our newest header minus a week.
232		let best_height = self.headers.iter().map(|(_, header)| header.height).max().unwrap_or(0);
233		let cutoff_height = best_height.saturating_sub(HEADER_CACHE_LIMIT);
234		self.headers.retain(|_, header| header.height >= cutoff_height);
235	}
236
237	/// Called when blocks have been disconnected from the best chain. Only the fork point
238	/// (best common ancestor) is provided.
239	///
240	/// Once disconnected, unless [`Self::retain_on_disconnect`] is set, a block's header is no
241	/// longer needed and thus can be removed.
242	pub(crate) fn blocks_disconnected(&mut self, fork_point: &ValidatedBlockHeader) {
243		if !self.retain_on_disconnect {
244			self.headers.retain(|_, block_info| block_info.height <= fork_point.height);
245		}
246	}
247}
248
249impl<P: Poll, L: Deref> SpvClient<P, L>
250where
251	L::Target: chain::Listen,
252{
253	/// Creates a new SPV client using `chain_tip` as the best known chain tip.
254	///
255	/// Subsequent calls to [`poll_best_tip`] will poll for the best chain tip using the given chain
256	/// poller, which may be configured with one or more block sources to query. At least one block
257	/// source must provide headers back from the best chain tip to its common ancestor with
258	/// `chain_tip`.
259	/// * `header_cache` is used to look up and store headers on the best chain
260	/// * `chain_listener` is notified of any blocks connected or disconnected
261	///
262	/// [`poll_best_tip`]: SpvClient::poll_best_tip
263	pub fn new(
264		chain_tip: ValidatedBlockHeader, chain_poller: P, header_cache: HeaderCache,
265		chain_listener: L,
266	) -> Self {
267		Self { chain_tip, chain_poller, header_cache, chain_listener }
268	}
269
270	/// Polls for the best tip and updates the chain listener with any connected or disconnected
271	/// blocks accordingly.
272	///
273	/// Returns the best polled chain tip relative to the previous best known tip and whether any
274	/// blocks were indeed connected or disconnected.
275	pub async fn poll_best_tip(&mut self) -> BlockSourceResult<(ChainTip, bool)> {
276		let chain_tip = self.chain_poller.poll_chain_tip(self.chain_tip).await?;
277		let blocks_connected = match chain_tip {
278			ChainTip::Common => false,
279			ChainTip::Better(chain_tip) => {
280				debug_assert_ne!(chain_tip.block_hash, self.chain_tip.block_hash);
281				debug_assert!(chain_tip.chainwork > self.chain_tip.chainwork);
282				self.update_chain_tip(chain_tip).await
283			},
284			ChainTip::Worse(chain_tip) => {
285				debug_assert_ne!(chain_tip.block_hash, self.chain_tip.block_hash);
286				debug_assert!(chain_tip.chainwork <= self.chain_tip.chainwork);
287				false
288			},
289		};
290		Ok((chain_tip, blocks_connected))
291	}
292
293	/// Updates the chain tip, syncing the chain listener with any connected or disconnected
294	/// blocks. Returns whether there were any such blocks.
295	async fn update_chain_tip(&mut self, best_chain_tip: ValidatedBlockHeader) -> bool {
296		let mut chain_notifier = ChainNotifier {
297			header_cache: &mut self.header_cache,
298			chain_listener: &*self.chain_listener,
299		};
300		match chain_notifier
301			.synchronize_listener(best_chain_tip, &self.chain_tip, &mut self.chain_poller)
302			.await
303		{
304			Ok(_) => {
305				self.chain_tip = best_chain_tip;
306				true
307			},
308			Err((_, Some(chain_tip))) if chain_tip.block_hash != self.chain_tip.block_hash => {
309				self.chain_tip = chain_tip;
310				true
311			},
312			Err(_) => false,
313		}
314	}
315}
316
317/// Notifies [listeners] of blocks that have been connected or disconnected from the chain.
318///
319/// [listeners]: lightning::chain::Listen
320pub(crate) struct ChainNotifier<'a, L: chain::Listen + ?Sized> {
321	/// Cache for looking up headers before fetching from a block source.
322	pub(crate) header_cache: &'a mut HeaderCache,
323
324	/// Listener that will be notified of connected or disconnected blocks.
325	pub(crate) chain_listener: &'a L,
326}
327
328/// Changes made to the chain between subsequent polls that transformed it from having one chain tip
329/// to another.
330///
331/// Blocks are given in height-descending order. Therefore, blocks are first disconnected in order
332/// before new blocks are connected in reverse order.
333struct ChainDifference {
334	/// The most recent ancestor common between the chain tips.
335	///
336	/// If there are any disconnected blocks, this is where the chain forked.
337	common_ancestor: ValidatedBlockHeader,
338
339	/// Blocks that were connected to the chain since the last poll.
340	connected_blocks: Vec<ValidatedBlockHeader>,
341}
342
343impl<'a, L: chain::Listen + ?Sized> ChainNotifier<'a, L> {
344	/// Finds the first common ancestor between `new_header` and `old_header`, disconnecting blocks
345	/// from `old_header` to get to that point and then connecting blocks until `new_header`.
346	///
347	/// Validates headers along the transition path, but doesn't fetch blocks until the chain is
348	/// disconnected to the fork point. Thus, this may return an `Err` that includes where the tip
349	/// ended up which may not be `new_header`. Note that the returned `Err` contains `Some` header
350	/// if and only if the transition from `old_header` to `new_header` is valid.
351	async fn synchronize_listener<P: Poll>(
352		&mut self, new_header: ValidatedBlockHeader, old_header: &ValidatedBlockHeader,
353		chain_poller: &mut P,
354	) -> Result<(), (BlockSourceError, Option<ValidatedBlockHeader>)> {
355		let difference = self
356			.find_difference_from_header(new_header, old_header, chain_poller)
357			.await
358			.map_err(|e| (e, None))?;
359		if difference.common_ancestor != *old_header {
360			self.disconnect_blocks(difference.common_ancestor);
361		}
362		self.connect_blocks(difference.common_ancestor, difference.connected_blocks, chain_poller)
363			.await
364	}
365
366	/// Returns the changes needed to produce the chain with `current_header` as its tip from the
367	/// chain with `prev_best_block` as its tip.
368	///
369	/// First resolves `prev_best_block` to a `ValidatedBlockHeader` using the `previous_blocks`
370	/// field as fallback if needed, then finds the common ancestor.
371	///
372	/// Updates the header cache as it goes, tracking headers needed to find the diff to reuse for
373	/// other objects that might need similar headers.
374	async fn find_difference_from_best_block<P: Poll>(
375		&mut self, current_header: ValidatedBlockHeader, prev_best_block: BlockLocator,
376		chain_poller: &mut P,
377	) -> BlockSourceResult<ChainDifference> {
378		// Try to resolve the header for the previous best block. First try the block_hash,
379		// then fall back to previous_blocks if that fails.
380		let cur_tip = core::iter::once((0, &prev_best_block.block_hash));
381		let prev_tips =
382			prev_best_block.previous_blocks.iter().enumerate().filter_map(|(idx, hash_opt)| {
383				if let Some(block_hash) = hash_opt {
384					Some((idx as u32 + 1, block_hash))
385				} else {
386					None
387				}
388			});
389		let mut found_header = None;
390		for (height_diff, block_hash) in cur_tip.chain(prev_tips) {
391			if let Some(header) = self.header_cache.look_up(block_hash) {
392				found_header = Some(*header);
393				break;
394			}
395			let height = prev_best_block.height.checked_sub(height_diff).ok_or(
396				BlockSourceError::persistent(
397					"BlockLocator had more previous_blocks than its height",
398				),
399			)?;
400			if let Ok(header) = chain_poller.get_header(block_hash, Some(height)).await {
401				found_header = Some(header);
402				self.header_cache.insert_during_diff(*block_hash, header);
403				break;
404			}
405		}
406		let found_header = found_header.ok_or_else(|| {
407			BlockSourceError::persistent("could not resolve any block from BlockLocator")
408		})?;
409
410		self.find_difference_from_header(current_header, &found_header, chain_poller).await
411	}
412
413	/// Returns the changes needed to produce the chain with `current_header` as its tip from the
414	/// chain with `prev_header` as its tip.
415	///
416	/// Walks backwards from `current_header` and `prev_header`, finding the common ancestor.
417	async fn find_difference_from_header<P: Poll>(
418		&self, current_header: ValidatedBlockHeader, prev_header: &ValidatedBlockHeader,
419		chain_poller: &mut P,
420	) -> BlockSourceResult<ChainDifference> {
421		let mut connected_blocks = Vec::new();
422		let mut current = current_header;
423		let mut previous = *prev_header;
424		loop {
425			// Found the common ancestor.
426			if current.block_hash == previous.block_hash {
427				break;
428			}
429
430			// Walk back the chain, finding blocks needed to connect and disconnect. Only walk back
431			// the header with the greater height, or both if equal heights.
432			let current_height = current.height;
433			let previous_height = previous.height;
434			if current_height <= previous_height {
435				previous = self.look_up_previous_header(chain_poller, &previous).await?;
436			}
437			if current_height >= previous_height {
438				connected_blocks.push(current);
439				current = self.look_up_previous_header(chain_poller, &current).await?;
440			}
441		}
442
443		let common_ancestor = current;
444		Ok(ChainDifference { common_ancestor, connected_blocks })
445	}
446
447	/// Returns the previous header for the given header, either by looking it up in the cache or
448	/// fetching it if not found.
449	async fn look_up_previous_header<P: Poll>(
450		&self, chain_poller: &mut P, header: &ValidatedBlockHeader,
451	) -> BlockSourceResult<ValidatedBlockHeader> {
452		match self.header_cache.look_up(&header.header.prev_blockhash) {
453			Some(prev_header) => Ok(*prev_header),
454			None => chain_poller.look_up_previous_header(header).await,
455		}
456	}
457
458	/// Notifies the chain listeners of disconnected blocks.
459	fn disconnect_blocks(&mut self, fork_point: ValidatedBlockHeader) {
460		self.header_cache.blocks_disconnected(&fork_point);
461		let best_block = BlockLocator::new(fork_point.block_hash, fork_point.height);
462		self.chain_listener.blocks_disconnected(best_block);
463	}
464
465	/// Notifies the chain listeners of connected blocks.
466	async fn connect_blocks<P: Poll>(
467		&mut self, mut new_tip: ValidatedBlockHeader,
468		mut connected_blocks: Vec<ValidatedBlockHeader>, chain_poller: &mut P,
469	) -> Result<(), (BlockSourceError, Option<ValidatedBlockHeader>)> {
470		for header in connected_blocks.drain(..).rev() {
471			let height = header.height;
472			let block_data =
473				chain_poller.fetch_block(&header).await.map_err(|e| (e, Some(new_tip)))?;
474			debug_assert_eq!(block_data.block_hash, header.block_hash);
475
476			match block_data.deref() {
477				BlockData::FullBlock(block) => {
478					self.chain_listener.block_connected(block, height);
479				},
480				BlockData::HeaderOnly(header) => {
481					self.chain_listener.filtered_block_connected(header, &[], height);
482				},
483			}
484
485			self.header_cache.block_connected(header.block_hash, header);
486			new_tip = header;
487		}
488
489		Ok(())
490	}
491}
492
493#[cfg(test)]
494mod spv_client_tests {
495	use super::*;
496	use crate::test_utils::{Blockchain, NullChainListener};
497
498	use bitcoin::network::Network;
499
500	#[tokio::test]
501	async fn poll_from_chain_without_headers() {
502		let mut chain = Blockchain::default().with_height(3).without_headers();
503		let best_tip = chain.at_height(1);
504
505		let poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
506		let cache = HeaderCache::new();
507		let mut listener = NullChainListener {};
508		let mut client = SpvClient::new(best_tip, poller, cache, &mut listener);
509		match client.poll_best_tip().await {
510			Err(e) => {
511				assert_eq!(e.kind(), BlockSourceErrorKind::Persistent);
512				assert_eq!(e.into_inner().as_ref().to_string(), "header not found");
513			},
514			Ok(_) => panic!("Expected error"),
515		}
516		assert_eq!(client.chain_tip, best_tip);
517	}
518
519	#[tokio::test]
520	async fn poll_from_chain_with_common_tip() {
521		let mut chain = Blockchain::default().with_height(3);
522		let common_tip = chain.tip();
523
524		let poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
525		let cache = HeaderCache::new();
526		let mut listener = NullChainListener {};
527		let mut client = SpvClient::new(common_tip, poller, cache, &mut listener);
528		match client.poll_best_tip().await {
529			Err(e) => panic!("Unexpected error: {:?}", e),
530			Ok((chain_tip, blocks_connected)) => {
531				assert_eq!(chain_tip, ChainTip::Common);
532				assert!(!blocks_connected);
533			},
534		}
535		assert_eq!(client.chain_tip, common_tip);
536	}
537
538	#[tokio::test]
539	async fn poll_from_chain_with_better_tip() {
540		let mut chain = Blockchain::default().with_height(3);
541		let new_tip = chain.tip();
542		let old_tip = chain.at_height(1);
543
544		let poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
545		let cache = HeaderCache::new();
546		let mut listener = NullChainListener {};
547		let mut client = SpvClient::new(old_tip, poller, cache, &mut listener);
548		match client.poll_best_tip().await {
549			Err(e) => panic!("Unexpected error: {:?}", e),
550			Ok((chain_tip, blocks_connected)) => {
551				assert_eq!(chain_tip, ChainTip::Better(new_tip));
552				assert!(blocks_connected);
553			},
554		}
555		assert_eq!(client.chain_tip, new_tip);
556	}
557
558	#[tokio::test]
559	async fn poll_from_chain_with_better_tip_and_without_any_new_blocks() {
560		let mut chain = Blockchain::default().with_height(3).without_blocks(2..);
561		let new_tip = chain.tip();
562		let old_tip = chain.at_height(1);
563
564		let poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
565		let cache = HeaderCache::new();
566		let mut listener = NullChainListener {};
567		let mut client = SpvClient::new(old_tip, poller, cache, &mut listener);
568		match client.poll_best_tip().await {
569			Err(e) => panic!("Unexpected error: {:?}", e),
570			Ok((chain_tip, blocks_connected)) => {
571				assert_eq!(chain_tip, ChainTip::Better(new_tip));
572				assert!(!blocks_connected);
573			},
574		}
575		assert_eq!(client.chain_tip, old_tip);
576	}
577
578	#[tokio::test]
579	async fn poll_from_chain_with_better_tip_and_without_some_new_blocks() {
580		let mut chain = Blockchain::default().with_height(3).without_blocks(3..);
581		let new_tip = chain.tip();
582		let old_tip = chain.at_height(1);
583
584		let poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
585		let cache = HeaderCache::new();
586		let mut listener = NullChainListener {};
587		let mut client = SpvClient::new(old_tip, poller, cache, &mut listener);
588		match client.poll_best_tip().await {
589			Err(e) => panic!("Unexpected error: {:?}", e),
590			Ok((chain_tip, blocks_connected)) => {
591				assert_eq!(chain_tip, ChainTip::Better(new_tip));
592				assert!(blocks_connected);
593			},
594		}
595		assert_eq!(client.chain_tip, chain.at_height(2));
596	}
597
598	#[tokio::test]
599	async fn poll_from_chain_with_worse_tip() {
600		let mut chain = Blockchain::default().with_height(3);
601		let best_tip = chain.tip();
602		chain.disconnect_tip();
603		let worse_tip = chain.tip();
604
605		let poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
606		let cache = HeaderCache::new();
607		let mut listener = NullChainListener {};
608		let mut client = SpvClient::new(best_tip, poller, cache, &mut listener);
609		match client.poll_best_tip().await {
610			Err(e) => panic!("Unexpected error: {:?}", e),
611			Ok((chain_tip, blocks_connected)) => {
612				assert_eq!(chain_tip, ChainTip::Worse(worse_tip));
613				assert!(!blocks_connected);
614			},
615		}
616		assert_eq!(client.chain_tip, best_tip);
617	}
618}
619
620#[cfg(test)]
621mod chain_notifier_tests {
622	use super::*;
623	use crate::test_utils::{Blockchain, MockChainListener};
624
625	use bitcoin::network::Network;
626
627	#[tokio::test]
628	async fn sync_from_same_chain() {
629		let mut chain = Blockchain::default().with_height(3);
630
631		let new_tip = chain.tip();
632		let old_tip = chain.at_height(1);
633		let chain_listener = &MockChainListener::new()
634			.expect_block_connected(*chain.at_height(2))
635			.expect_block_connected(*new_tip);
636		let mut notifier =
637			ChainNotifier { header_cache: &mut chain.header_cache(0..=1), chain_listener };
638		let mut poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
639		match notifier.synchronize_listener(new_tip, &old_tip, &mut poller).await {
640			Err((e, _)) => panic!("Unexpected error: {:?}", e),
641			Ok(_) => {},
642		}
643	}
644
645	#[tokio::test]
646	async fn sync_from_different_chains() {
647		let mut test_chain = Blockchain::with_network(Network::Testnet).with_height(1);
648		let main_chain = Blockchain::with_network(Network::Bitcoin).with_height(1);
649
650		let new_tip = test_chain.tip();
651		let old_tip = main_chain.tip();
652		let chain_listener = &MockChainListener::new();
653		let mut notifier =
654			ChainNotifier { header_cache: &mut main_chain.header_cache(0..=1), chain_listener };
655		let mut poller = poll::ChainPoller::new(&mut test_chain, Network::Testnet);
656		match notifier.synchronize_listener(new_tip, &old_tip, &mut poller).await {
657			Err((e, _)) => {
658				assert_eq!(e.kind(), BlockSourceErrorKind::Persistent);
659				assert_eq!(e.into_inner().as_ref().to_string(), "genesis block reached");
660			},
661			Ok(_) => panic!("Expected error"),
662		}
663	}
664
665	#[tokio::test]
666	async fn sync_from_equal_length_fork() {
667		let main_chain = Blockchain::default().with_height(2);
668		let mut fork_chain = main_chain.fork_at_height(1);
669
670		let new_tip = fork_chain.tip();
671		let old_tip = main_chain.tip();
672		let chain_listener = &MockChainListener::new()
673			.expect_blocks_disconnected(*fork_chain.at_height(1))
674			.expect_block_connected(*new_tip);
675		let mut notifier =
676			ChainNotifier { header_cache: &mut main_chain.header_cache(0..=2), chain_listener };
677		let mut poller = poll::ChainPoller::new(&mut fork_chain, Network::Testnet);
678		match notifier.synchronize_listener(new_tip, &old_tip, &mut poller).await {
679			Err((e, _)) => panic!("Unexpected error: {:?}", e),
680			Ok(_) => {},
681		}
682	}
683
684	#[tokio::test]
685	async fn sync_from_shorter_fork() {
686		let main_chain = Blockchain::default().with_height(3);
687		let mut fork_chain = main_chain.fork_at_height(1);
688		fork_chain.disconnect_tip();
689
690		let new_tip = fork_chain.tip();
691		let old_tip = main_chain.tip();
692		let chain_listener = &MockChainListener::new()
693			.expect_blocks_disconnected(*main_chain.at_height(1))
694			.expect_block_connected(*new_tip);
695		let mut notifier =
696			ChainNotifier { header_cache: &mut main_chain.header_cache(0..=3), chain_listener };
697		let mut poller = poll::ChainPoller::new(&mut fork_chain, Network::Testnet);
698		match notifier.synchronize_listener(new_tip, &old_tip, &mut poller).await {
699			Err((e, _)) => panic!("Unexpected error: {:?}", e),
700			Ok(_) => {},
701		}
702	}
703
704	#[tokio::test]
705	async fn sync_from_longer_fork() {
706		let mut main_chain = Blockchain::default().with_height(3);
707		let mut fork_chain = main_chain.fork_at_height(1);
708		main_chain.disconnect_tip();
709
710		let new_tip = fork_chain.tip();
711		let old_tip = main_chain.tip();
712		let chain_listener = &MockChainListener::new()
713			.expect_blocks_disconnected(*fork_chain.at_height(1))
714			.expect_block_connected(*fork_chain.at_height(2))
715			.expect_block_connected(*new_tip);
716		let mut notifier =
717			ChainNotifier { header_cache: &mut main_chain.header_cache(0..=2), chain_listener };
718		let mut poller = poll::ChainPoller::new(&mut fork_chain, Network::Testnet);
719		match notifier.synchronize_listener(new_tip, &old_tip, &mut poller).await {
720			Err((e, _)) => panic!("Unexpected error: {:?}", e),
721			Ok(_) => {},
722		}
723	}
724
725	#[tokio::test]
726	async fn sync_from_chain_without_headers() {
727		let mut chain = Blockchain::default().with_height(3).without_headers();
728
729		let new_tip = chain.tip();
730		let old_tip = chain.at_height(1);
731		let chain_listener = &MockChainListener::new();
732		let mut notifier =
733			ChainNotifier { header_cache: &mut chain.header_cache(0..=1), chain_listener };
734		let mut poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
735		match notifier.synchronize_listener(new_tip, &old_tip, &mut poller).await {
736			Err((_, tip)) => assert_eq!(tip, None),
737			Ok(_) => panic!("Expected error"),
738		}
739	}
740
741	#[tokio::test]
742	async fn sync_from_chain_without_any_new_blocks() {
743		let mut chain = Blockchain::default().with_height(3).without_blocks(2..);
744
745		let new_tip = chain.tip();
746		let old_tip = chain.at_height(1);
747		let chain_listener = &MockChainListener::new();
748		let mut notifier =
749			ChainNotifier { header_cache: &mut chain.header_cache(0..=3), chain_listener };
750		let mut poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
751		match notifier.synchronize_listener(new_tip, &old_tip, &mut poller).await {
752			Err((_, tip)) => assert_eq!(tip, Some(old_tip)),
753			Ok(_) => panic!("Expected error"),
754		}
755	}
756
757	#[tokio::test]
758	async fn sync_from_chain_without_some_new_blocks() {
759		let mut chain = Blockchain::default().with_height(3).without_blocks(3..);
760
761		let new_tip = chain.tip();
762		let old_tip = chain.at_height(1);
763		let chain_listener = &MockChainListener::new().expect_block_connected(*chain.at_height(2));
764		let mut notifier =
765			ChainNotifier { header_cache: &mut chain.header_cache(0..=3), chain_listener };
766		let mut poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
767		match notifier.synchronize_listener(new_tip, &old_tip, &mut poller).await {
768			Err((_, tip)) => assert_eq!(tip, Some(chain.at_height(2))),
769			Ok(_) => panic!("Expected error"),
770		}
771	}
772
773	#[tokio::test]
774	async fn sync_from_chain_with_filtered_blocks() {
775		let mut chain = Blockchain::default().with_height(3).filtered_blocks();
776
777		let new_tip = chain.tip();
778		let old_tip = chain.at_height(1);
779		let chain_listener = &MockChainListener::new()
780			.expect_filtered_block_connected(*chain.at_height(2))
781			.expect_filtered_block_connected(*new_tip);
782		let mut notifier =
783			ChainNotifier { header_cache: &mut chain.header_cache(0..=1), chain_listener };
784		let mut poller = poll::ChainPoller::new(&mut chain, Network::Testnet);
785		match notifier.synchronize_listener(new_tip, &old_tip, &mut poller).await {
786			Err((e, _)) => panic!("Unexpected error: {:?}", e),
787			Ok(_) => {},
788		}
789	}
790}