11//! A lightweight client for keeping in sync with chain activity.
22//!
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+ //!
37//! Defines a [`BlockSource`] trait, which is an asynchronous interface for retrieving block headers
48//! and data.
59//!
913//! Both features support either blocking I/O using `std::net::TcpStream` or, with feature `tokio`,
1014//! non-blocking I/O using `tokio::net::TcpStream` from inside a Tokio runtime.
1115//!
16+ //! [`SpvClient`]: struct.SpvClient.html
1217//! [`BlockSource`]: trait.BlockSource.html
1318
1419#[ cfg( any( feature = "rest-client" , feature = "rpc-client" ) ) ]
@@ -31,7 +36,7 @@ mod test_utils;
3136#[ cfg( any( feature = "rest-client" , feature = "rpc-client" ) ) ]
3237mod utils;
3338
34- use crate :: poll:: { Poll , ValidatedBlockHeader } ;
39+ use crate :: poll:: { ChainTip , Poll , ValidatedBlockHeader } ;
3540
3641use bitcoin:: blockdata:: block:: { Block , BlockHeader } ;
3742use bitcoin:: hash_types:: BlockHash ;
@@ -54,9 +59,13 @@ pub trait BlockSource : Sync + Send {
5459 /// error.
5560 fn get_block < ' a > ( & ' a mut self , header_hash : & ' a BlockHash ) -> AsyncBlockSourceResult < ' a , Block > ;
5661
57- // TODO: Phrase in terms of `Poll` once added.
58- /// Returns the hash of the best block and, optionally, its height. When polling a block source,
59- /// the height is passed to `get_header` to allow for a more efficient lookup.
62+ /// Returns the hash of the best block and, optionally, its height.
63+ ///
64+ /// When polling a block source, [`Poll`] implementations may pass the height to [`get_header`]
65+ /// to allow for a more efficient lookup.
66+ ///
67+ /// [`Poll`]: poll/trait.Poll.html
68+ /// [`get_header`]: #tymethod.get_header
6069 fn get_best_block < ' a > ( & ' a mut self ) -> AsyncBlockSourceResult < ( BlockHash , Option < u32 > ) > ;
6170}
6271
@@ -133,6 +142,25 @@ pub struct BlockHeaderData {
133142 pub chainwork : Uint256 ,
134143}
135144
145+ /// A lightweight client for keeping a listener in sync with the chain, allowing for Simplified
146+ /// Payment Verification (SPV).
147+ ///
148+ /// The client is parameterized by a chain poller which is responsible for polling one or more block
149+ /// sources for the best chain tip. During this process it detects any chain forks, determines which
150+ /// constitutes the best chain, and updates the listener accordingly with any blocks that were
151+ /// connected or disconnected since the last poll.
152+ ///
153+ /// Block headers for the best chain are maintained in the parameterized cache, allowing for a
154+ /// custom cache eviction policy. This offers flexibility to those sensitive to resource usage.
155+ /// Hence, there is a trade-off between a lower memory footprint and potentially increased network
156+ /// I/O as headers are re-fetched during fork detection.
157+ pub struct SpvClient < P : Poll , C : Cache , L : ChainListener > {
158+ chain_tip : ValidatedBlockHeader ,
159+ chain_poller : P ,
160+ chain_notifier : ChainNotifier < C > ,
161+ chain_listener : L ,
162+ }
163+
136164/// Adaptor used for notifying when blocks have been connected or disconnected from the chain.
137165///
138166/// Used when needing to replay chain data upon startup or as new chain events occur.
@@ -186,6 +214,69 @@ impl Cache for UnboundedCache {
186214 }
187215}
188216
217+ impl < P : Poll , C : Cache , L : ChainListener > SpvClient < P , C , L > {
218+ /// Creates a new SPV client using `chain_tip` as the best known chain tip.
219+ ///
220+ /// Subsequent calls to [`poll_best_tip`] will poll for the best chain tip using the given chain
221+ /// poller, which may be configured with one or more block sources to query. At least one block
222+ /// source must provide headers back from the best chain tip to its common ancestor with
223+ /// `chain_tip`.
224+ /// * `header_cache` is used to look up and store headers on the best chain
225+ /// * `chain_listener` is notified of any blocks connected or disconnected
226+ ///
227+ /// [`poll_best_tip`]: struct.SpvClient.html#method.poll_best_tip
228+ pub fn new (
229+ chain_tip : ValidatedBlockHeader ,
230+ chain_poller : P ,
231+ header_cache : C ,
232+ chain_listener : L ,
233+ ) -> Self {
234+ let chain_notifier = ChainNotifier { header_cache } ;
235+ Self { chain_tip, chain_poller, chain_notifier, chain_listener }
236+ }
237+
238+ /// Polls for the best tip and updates the chain listener with any connected or disconnected
239+ /// blocks accordingly.
240+ ///
241+ /// Returns the best polled chain tip relative to the previous best known tip and whether any
242+ /// blocks were indeed connected or disconnected.
243+ pub async fn poll_best_tip ( & mut self ) -> BlockSourceResult < ( ChainTip , bool ) > {
244+ let chain_tip = self . chain_poller . poll_chain_tip ( self . chain_tip ) . await ?;
245+ let blocks_connected = match chain_tip {
246+ ChainTip :: Common => false ,
247+ ChainTip :: Better ( chain_tip) => {
248+ debug_assert_ne ! ( chain_tip. block_hash, self . chain_tip. block_hash) ;
249+ debug_assert ! ( chain_tip. chainwork > self . chain_tip. chainwork) ;
250+ self . update_chain_tip ( chain_tip) . await
251+ } ,
252+ ChainTip :: Worse ( chain_tip) => {
253+ debug_assert_ne ! ( chain_tip. block_hash, self . chain_tip. block_hash) ;
254+ debug_assert ! ( chain_tip. chainwork <= self . chain_tip. chainwork) ;
255+ false
256+ } ,
257+ } ;
258+ Ok ( ( chain_tip, blocks_connected) )
259+ }
260+
261+ /// Updates the chain tip, syncing the chain listener with any connected or disconnected
262+ /// blocks. Returns whether there were any such blocks.
263+ async fn update_chain_tip ( & mut self , best_chain_tip : ValidatedBlockHeader ) -> bool {
264+ match self . chain_notifier . synchronize_listener (
265+ best_chain_tip, & self . chain_tip , & mut self . chain_poller , & mut self . chain_listener ) . await
266+ {
267+ Ok ( _) => {
268+ self . chain_tip = best_chain_tip;
269+ true
270+ } ,
271+ Err ( ( _, Some ( chain_tip) ) ) if chain_tip. block_hash != self . chain_tip . block_hash => {
272+ self . chain_tip = chain_tip;
273+ true
274+ } ,
275+ Err ( _) => false ,
276+ }
277+ }
278+ }
279+
189280/// Notifies [listeners] of blocks that have been connected or disconnected from the chain.
190281///
191282/// [listeners]: trait.ChainListener.html
@@ -299,6 +390,127 @@ impl<C: Cache> ChainNotifier<C> {
299390 }
300391}
301392
393+ #[ cfg( test) ]
394+ mod spv_client_tests {
395+ use crate :: test_utils:: { Blockchain , NullChainListener } ;
396+ use super :: * ;
397+
398+ use bitcoin:: network:: constants:: Network ;
399+
400+ #[ tokio:: test]
401+ async fn poll_from_chain_without_headers ( ) {
402+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) . without_headers ( ) ;
403+ let best_tip = chain. at_height ( 1 ) ;
404+
405+ let poller = poll:: ChainPoller :: new ( & mut chain, Network :: Testnet ) ;
406+ let cache = UnboundedCache :: new ( ) ;
407+ let mut client = SpvClient :: new ( best_tip, poller, cache, NullChainListener { } ) ;
408+ match client. poll_best_tip ( ) . await {
409+ Err ( e) => {
410+ assert_eq ! ( e. kind( ) , BlockSourceErrorKind :: Persistent ) ;
411+ assert_eq ! ( e. into_inner( ) . as_ref( ) . to_string( ) , "header not found" ) ;
412+ } ,
413+ Ok ( _) => panic ! ( "Expected error" ) ,
414+ }
415+ assert_eq ! ( client. chain_tip, best_tip) ;
416+ }
417+
418+ #[ tokio:: test]
419+ async fn poll_from_chain_with_common_tip ( ) {
420+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) ;
421+ let common_tip = chain. tip ( ) ;
422+
423+ let poller = poll:: ChainPoller :: new ( & mut chain, Network :: Testnet ) ;
424+ let cache = UnboundedCache :: new ( ) ;
425+ let mut client = SpvClient :: new ( common_tip, poller, cache, NullChainListener { } ) ;
426+ match client. poll_best_tip ( ) . await {
427+ Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
428+ Ok ( ( chain_tip, blocks_connected) ) => {
429+ assert_eq ! ( chain_tip, ChainTip :: Common ) ;
430+ assert ! ( !blocks_connected) ;
431+ } ,
432+ }
433+ assert_eq ! ( client. chain_tip, common_tip) ;
434+ }
435+
436+ #[ tokio:: test]
437+ async fn poll_from_chain_with_better_tip ( ) {
438+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) ;
439+ let new_tip = chain. tip ( ) ;
440+ let old_tip = chain. at_height ( 1 ) ;
441+
442+ let poller = poll:: ChainPoller :: new ( & mut chain, Network :: Testnet ) ;
443+ let cache = UnboundedCache :: new ( ) ;
444+ let mut client = SpvClient :: new ( old_tip, poller, cache, NullChainListener { } ) ;
445+ match client. poll_best_tip ( ) . await {
446+ Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
447+ Ok ( ( chain_tip, blocks_connected) ) => {
448+ assert_eq ! ( chain_tip, ChainTip :: Better ( new_tip) ) ;
449+ assert ! ( blocks_connected) ;
450+ } ,
451+ }
452+ assert_eq ! ( client. chain_tip, new_tip) ;
453+ }
454+
455+ #[ tokio:: test]
456+ async fn poll_from_chain_with_better_tip_and_without_any_new_blocks ( ) {
457+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) . without_blocks ( 2 ..) ;
458+ let new_tip = chain. tip ( ) ;
459+ let old_tip = chain. at_height ( 1 ) ;
460+
461+ let poller = poll:: ChainPoller :: new ( & mut chain, Network :: Testnet ) ;
462+ let cache = UnboundedCache :: new ( ) ;
463+ let mut client = SpvClient :: new ( old_tip, poller, cache, NullChainListener { } ) ;
464+ match client. poll_best_tip ( ) . await {
465+ Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
466+ Ok ( ( chain_tip, blocks_connected) ) => {
467+ assert_eq ! ( chain_tip, ChainTip :: Better ( new_tip) ) ;
468+ assert ! ( !blocks_connected) ;
469+ } ,
470+ }
471+ assert_eq ! ( client. chain_tip, old_tip) ;
472+ }
473+
474+ #[ tokio:: test]
475+ async fn poll_from_chain_with_better_tip_and_without_some_new_blocks ( ) {
476+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) . without_blocks ( 3 ..) ;
477+ let new_tip = chain. tip ( ) ;
478+ let old_tip = chain. at_height ( 1 ) ;
479+
480+ let poller = poll:: ChainPoller :: new ( & mut chain, Network :: Testnet ) ;
481+ let cache = UnboundedCache :: new ( ) ;
482+ let mut client = SpvClient :: new ( old_tip, poller, cache, NullChainListener { } ) ;
483+ match client. poll_best_tip ( ) . await {
484+ Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
485+ Ok ( ( chain_tip, blocks_connected) ) => {
486+ assert_eq ! ( chain_tip, ChainTip :: Better ( new_tip) ) ;
487+ assert ! ( blocks_connected) ;
488+ } ,
489+ }
490+ assert_eq ! ( client. chain_tip, chain. at_height( 2 ) ) ;
491+ }
492+
493+ #[ tokio:: test]
494+ async fn poll_from_chain_with_worse_tip ( ) {
495+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) ;
496+ let best_tip = chain. tip ( ) ;
497+ chain. disconnect_tip ( ) ;
498+ let worse_tip = chain. tip ( ) ;
499+
500+ let poller = poll:: ChainPoller :: new ( & mut chain, Network :: Testnet ) ;
501+ let cache = UnboundedCache :: new ( ) ;
502+ let mut client = SpvClient :: new ( best_tip, poller, cache, NullChainListener { } ) ;
503+ match client. poll_best_tip ( ) . await {
504+ Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
505+ Ok ( ( chain_tip, blocks_connected) ) => {
506+ assert_eq ! ( chain_tip, ChainTip :: Worse ( worse_tip) ) ;
507+ assert ! ( !blocks_connected) ;
508+ } ,
509+ }
510+ assert_eq ! ( client. chain_tip, best_tip) ;
511+ }
512+ }
513+
302514#[ cfg( test) ]
303515mod chain_notifier_tests {
304516 use crate :: test_utils:: { Blockchain , MockChainListener } ;
0 commit comments