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 ;
@@ -133,6 +138,25 @@ pub struct BlockHeaderData {
133
138
pub chainwork : Uint256 ,
134
139
}
135
140
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
+
136
160
/// Adaptor used for notifying when blocks have been connected or disconnected from the chain.
137
161
///
138
162
/// Used when needing to replay chain data upon startup or as new chain events occur.
@@ -179,6 +203,67 @@ impl Cache for UnboundedCache {
179
203
}
180
204
}
181
205
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
+
182
267
/// Notifies [listeners] of blocks that have been connected or disconnected from the chain.
183
268
///
184
269
/// [listeners]: trait.ChainListener.html
@@ -318,6 +403,127 @@ impl<C: Cache> ChainNotifier<C> {
318
403
}
319
404
}
320
405
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
+
321
527
#[ cfg( test) ]
322
528
mod chain_notifier_tests {
323
529
use crate :: test_utils:: { Blockchain , MockChainListener } ;
0 commit comments