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 ;
@@ -133,6 +138,25 @@ pub struct BlockHeaderData {
133138 pub chainwork : Uint256 ,
134139}
135140
141+ /// A lightweight client for keeping a listener in sync with the chain, allowing for Simplified
142+ /// Payment Verification (SPV).
143+ ///
144+ /// The client is parameterized by a chain poller which is responsible for polling one or more block
145+ /// sources for the best chain tip. During this process it detects any chain forks, determines which
146+ /// constitutes the best chain, and updates the listener accordingly with any blocks that were
147+ /// connected or disconnected since the last poll.
148+ ///
149+ /// Block headers for the best chain are maintained in the parameterized cache, allowing for a
150+ /// custom cache eviction policy. This offers flexibility to those sensitive to resource usage.
151+ /// Hence, there is a trade-off between a lower memory footprint and potentially increased network
152+ /// I/O as headers are re-fetched during fork detection.
153+ pub struct SpvClient < P : Poll , C : Cache , L : ChainListener > {
154+ chain_tip : ValidatedBlockHeader ,
155+ chain_poller : P ,
156+ chain_notifier : ChainNotifier < C > ,
157+ chain_listener : L ,
158+ }
159+
136160/// Adaptor used for notifying when blocks have been connected or disconnected from the chain.
137161///
138162/// Used when needing to replay chain data upon startup or as new chain events occur.
@@ -179,6 +203,67 @@ impl Cache for UnboundedCache {
179203 }
180204}
181205
206+ impl < P : Poll , C : Cache , L : ChainListener > SpvClient < P , C , L > {
207+ /// Creates a new SPV client using `chain_tip` as the best known chain tip.
208+ ///
209+ /// Subsequent calls to [`poll_best_tip`] will poll for the best chain tip using the given chain
210+ /// poller, which may be configured with one or more block sources to query. At least one block
211+ /// source must provide headers back from the best chain tip to its common ancestor with
212+ /// `chain_tip`.
213+ /// * `header_cache` is used to look up and store headers on the best chain
214+ /// * `chain_listener` is notified of any blocks connected or disconnected
215+ ///
216+ /// [`poll_best_tip`]: struct.SpvClient.html#method.poll_best_tip
217+ pub fn new (
218+ chain_tip : ValidatedBlockHeader ,
219+ chain_poller : P ,
220+ header_cache : C ,
221+ chain_listener : L ,
222+ ) -> Self {
223+ let chain_notifier = ChainNotifier { header_cache } ;
224+ Self { chain_tip, chain_poller, chain_notifier, chain_listener }
225+ }
226+
227+ /// Polls for the best tip and updates the chain listener with any connected or disconnected
228+ /// blocks accordingly.
229+ ///
230+ /// Returns the best polled chain tip relative to the previous best known tip and whether any
231+ /// blocks were indeed connected or disconnected.
232+ pub async fn poll_best_tip ( & mut self ) -> BlockSourceResult < ( ChainTip , bool ) > {
233+ let chain_tip = self . chain_poller . poll_chain_tip ( self . chain_tip ) . await ?;
234+ let blocks_connected = match chain_tip {
235+ ChainTip :: Common => false ,
236+ ChainTip :: Better ( chain_tip) => {
237+ debug_assert_ne ! ( chain_tip. block_hash, self . chain_tip. block_hash) ;
238+ debug_assert ! ( chain_tip. chainwork > self . chain_tip. chainwork) ;
239+ self . update_chain_tip ( chain_tip) . await
240+ } ,
241+ ChainTip :: Worse ( chain_tip) => {
242+ debug_assert_ne ! ( chain_tip. block_hash, self . chain_tip. block_hash) ;
243+ debug_assert ! ( chain_tip. chainwork <= self . chain_tip. chainwork) ;
244+ false
245+ } ,
246+ } ;
247+ Ok ( ( chain_tip, blocks_connected) )
248+ }
249+
250+ /// Updates the chain tip, syncing the chain listener with any connected or disconnected
251+ /// blocks. Returns whether there were any such blocks.
252+ async fn update_chain_tip ( & mut self , best_chain_tip : ValidatedBlockHeader ) -> bool {
253+ match self . chain_notifier . sync_listener ( best_chain_tip, & self . chain_tip , & mut self . chain_poller , & mut self . chain_listener ) . await {
254+ Ok ( _) => {
255+ self . chain_tip = best_chain_tip;
256+ true
257+ } ,
258+ Err ( ( _, Some ( chain_tip) ) ) if chain_tip. block_hash != self . chain_tip . block_hash => {
259+ self . chain_tip = chain_tip;
260+ true
261+ } ,
262+ Err ( _) => false ,
263+ }
264+ }
265+ }
266+
182267/// Notifies [listeners] of blocks that have been connected or disconnected from the chain.
183268///
184269/// [listeners]: trait.ChainListener.html
@@ -318,6 +403,127 @@ impl<C: Cache> ChainNotifier<C> {
318403 }
319404}
320405
406+ #[ cfg( test) ]
407+ mod spv_client_tests {
408+ use crate :: test_utils:: { Blockchain , NullChainListener } ;
409+ use super :: * ;
410+
411+ use bitcoin:: network:: constants:: Network ;
412+
413+ #[ tokio:: test]
414+ async fn poll_from_chain_without_headers ( ) {
415+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) . without_headers ( ) ;
416+ let best_tip = chain. at_height ( 1 ) ;
417+
418+ let poller = poll:: ChainPoller :: new ( & mut chain as & mut dyn BlockSource , Network :: Testnet ) ;
419+ let cache = UnboundedCache :: new ( ) ;
420+ let mut client = SpvClient :: new ( best_tip, poller, cache, NullChainListener { } ) ;
421+ match client. poll_best_tip ( ) . await {
422+ Err ( e) => {
423+ assert_eq ! ( e. kind( ) , BlockSourceErrorKind :: Persistent ) ;
424+ assert_eq ! ( e. into_inner( ) . as_ref( ) . to_string( ) , "header not found" ) ;
425+ } ,
426+ Ok ( _) => panic ! ( "Expected error" ) ,
427+ }
428+ assert_eq ! ( client. chain_tip, best_tip) ;
429+ }
430+
431+ #[ tokio:: test]
432+ async fn poll_from_chain_with_common_tip ( ) {
433+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) ;
434+ let common_tip = chain. tip ( ) ;
435+
436+ let poller = poll:: ChainPoller :: new ( & mut chain as & mut dyn BlockSource , Network :: Testnet ) ;
437+ let cache = UnboundedCache :: new ( ) ;
438+ let mut client = SpvClient :: new ( common_tip, poller, cache, NullChainListener { } ) ;
439+ match client. poll_best_tip ( ) . await {
440+ Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
441+ Ok ( ( chain_tip, blocks_connected) ) => {
442+ assert_eq ! ( chain_tip, ChainTip :: Common ) ;
443+ assert ! ( !blocks_connected) ;
444+ } ,
445+ }
446+ assert_eq ! ( client. chain_tip, common_tip) ;
447+ }
448+
449+ #[ tokio:: test]
450+ async fn poll_from_chain_with_better_tip ( ) {
451+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) ;
452+ let new_tip = chain. tip ( ) ;
453+ let old_tip = chain. at_height ( 1 ) ;
454+
455+ let poller = poll:: ChainPoller :: new ( & mut chain as & mut dyn BlockSource , Network :: Testnet ) ;
456+ let cache = UnboundedCache :: new ( ) ;
457+ let mut client = SpvClient :: new ( old_tip, poller, cache, NullChainListener { } ) ;
458+ match client. poll_best_tip ( ) . await {
459+ Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
460+ Ok ( ( chain_tip, blocks_connected) ) => {
461+ assert_eq ! ( chain_tip, ChainTip :: Better ( new_tip) ) ;
462+ assert ! ( blocks_connected) ;
463+ } ,
464+ }
465+ assert_eq ! ( client. chain_tip, new_tip) ;
466+ }
467+
468+ #[ tokio:: test]
469+ async fn poll_from_chain_with_better_tip_and_without_any_new_blocks ( ) {
470+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) . without_blocks ( 2 ..) ;
471+ let new_tip = chain. tip ( ) ;
472+ let old_tip = chain. at_height ( 1 ) ;
473+
474+ let poller = poll:: ChainPoller :: new ( & mut chain as & mut dyn BlockSource , Network :: Testnet ) ;
475+ let cache = UnboundedCache :: new ( ) ;
476+ let mut client = SpvClient :: new ( old_tip, poller, cache, NullChainListener { } ) ;
477+ match client. poll_best_tip ( ) . await {
478+ Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
479+ Ok ( ( chain_tip, blocks_connected) ) => {
480+ assert_eq ! ( chain_tip, ChainTip :: Better ( new_tip) ) ;
481+ assert ! ( !blocks_connected) ;
482+ } ,
483+ }
484+ assert_eq ! ( client. chain_tip, old_tip) ;
485+ }
486+
487+ #[ tokio:: test]
488+ async fn poll_from_chain_with_better_tip_and_without_some_new_blocks ( ) {
489+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) . without_blocks ( 3 ..) ;
490+ let new_tip = chain. tip ( ) ;
491+ let old_tip = chain. at_height ( 1 ) ;
492+
493+ let poller = poll:: ChainPoller :: new ( & mut chain as & mut dyn BlockSource , Network :: Testnet ) ;
494+ let cache = UnboundedCache :: new ( ) ;
495+ let mut client = SpvClient :: new ( old_tip, poller, cache, NullChainListener { } ) ;
496+ match client. poll_best_tip ( ) . await {
497+ Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
498+ Ok ( ( chain_tip, blocks_connected) ) => {
499+ assert_eq ! ( chain_tip, ChainTip :: Better ( new_tip) ) ;
500+ assert ! ( blocks_connected) ;
501+ } ,
502+ }
503+ assert_eq ! ( client. chain_tip, chain. at_height( 2 ) ) ;
504+ }
505+
506+ #[ tokio:: test]
507+ async fn poll_from_chain_with_worse_tip ( ) {
508+ let mut chain = Blockchain :: default ( ) . with_height ( 3 ) ;
509+ let best_tip = chain. tip ( ) ;
510+ chain. disconnect_tip ( ) ;
511+ let worse_tip = chain. tip ( ) ;
512+
513+ let poller = poll:: ChainPoller :: new ( & mut chain as & mut dyn BlockSource , Network :: Testnet ) ;
514+ let cache = UnboundedCache :: new ( ) ;
515+ let mut client = SpvClient :: new ( best_tip, poller, cache, NullChainListener { } ) ;
516+ match client. poll_best_tip ( ) . await {
517+ Err ( e) => panic ! ( "Unexpected error: {:?}" , e) ,
518+ Ok ( ( chain_tip, blocks_connected) ) => {
519+ assert_eq ! ( chain_tip, ChainTip :: Worse ( worse_tip) ) ;
520+ assert ! ( !blocks_connected) ;
521+ } ,
522+ }
523+ assert_eq ! ( client. chain_tip, best_tip) ;
524+ }
525+ }
526+
321527#[ cfg( test) ]
322528mod chain_notifier_tests {
323529 use crate :: test_utils:: { Blockchain , MockChainListener } ;
0 commit comments