1
1
//! A lightweight client for keeping in sync with chain activity.
2
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
+ //!
3
7
//! Defines a [`BlockSource`] trait, which is an asynchronous interface for retrieving block headers
4
8
//! and data.
5
9
//!
9
13
//! Both features support either blocking I/O using `std::net::TcpStream` or, with feature `tokio`,
10
14
//! non-blocking I/O using `tokio::net::TcpStream` from inside a Tokio runtime.
11
15
//!
16
+ //! [`SpvClient`]: struct.SpvClient.html
12
17
//! [`BlockSource`]: trait.BlockSource.html
13
18
14
19
#[ cfg( any( feature = "rest-client" , feature = "rpc-client" ) ) ]
@@ -31,7 +36,7 @@ mod test_utils;
31
36
#[ cfg( any( feature = "rest-client" , feature = "rpc-client" ) ) ]
32
37
mod utils;
33
38
34
- use crate :: poll:: { Poll , ValidatedBlockHeader } ;
39
+ use crate :: poll:: { ChainTip , Poll , ValidatedBlockHeader } ;
35
40
36
41
use bitcoin:: blockdata:: block:: { Block , BlockHeader } ;
37
42
use bitcoin:: hash_types:: BlockHash ;
@@ -54,9 +59,13 @@ pub trait BlockSource : Sync + Send {
54
59
/// error.
55
60
fn get_block < ' a > ( & ' a mut self , header_hash : & ' a BlockHash ) -> AsyncBlockSourceResult < ' a , Block > ;
56
61
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
60
69
fn get_best_block < ' a > ( & ' a mut self ) -> AsyncBlockSourceResult < ( BlockHash , Option < u32 > ) > ;
61
70
}
62
71
@@ -133,6 +142,25 @@ pub struct BlockHeaderData {
133
142
pub chainwork : Uint256 ,
134
143
}
135
144
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
+
136
164
/// Adaptor used for notifying when blocks have been connected or disconnected from the chain.
137
165
///
138
166
/// Used when needing to replay chain data upon startup or as new chain events occur.
@@ -186,6 +214,67 @@ impl Cache for UnboundedCache {
186
214
}
187
215
}
188
216
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 . sync_listener ( best_chain_tip, & self . chain_tip , & mut self . chain_poller , & mut self . chain_listener ) . await {
265
+ Ok ( _) => {
266
+ self . chain_tip = best_chain_tip;
267
+ true
268
+ } ,
269
+ Err ( ( _, Some ( chain_tip) ) ) if chain_tip. block_hash != self . chain_tip . block_hash => {
270
+ self . chain_tip = chain_tip;
271
+ true
272
+ } ,
273
+ Err ( _) => false ,
274
+ }
275
+ }
276
+ }
277
+
189
278
/// Notifies [listeners] of blocks that have been connected or disconnected from the chain.
190
279
///
191
280
/// [listeners]: trait.ChainListener.html
@@ -301,6 +390,127 @@ impl<C: Cache> ChainNotifier<C> {
301
390
}
302
391
}
303
392
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
+
304
514
#[ cfg( test) ]
305
515
mod chain_notifier_tests {
306
516
use crate :: test_utils:: { Blockchain , MockChainListener } ;
0 commit comments