1
1
use crate :: logger:: {
2
2
log_error, log_given_level, log_internal, log_trace, FilesystemLogger , Logger ,
3
3
} ;
4
- use crate :: Error ;
4
+ use crate :: { Config , Error } ;
5
5
6
6
use lightning:: chain:: chaininterface:: { BroadcasterInterface , ConfirmationTarget , FeeEstimator } ;
7
7
use lightning:: chain:: WatchedOutput ;
8
8
use lightning:: chain:: { Confirm , Filter } ;
9
9
10
- use bdk:: blockchain:: { Blockchain , EsploraBlockchain , GetBlockHash , GetHeight , GetTx } ;
10
+ use bdk:: blockchain:: EsploraBlockchain ;
11
11
use bdk:: database:: BatchDatabase ;
12
+ use bdk:: esplora_client;
12
13
use bdk:: wallet:: AddressIndex ;
13
- use bdk:: { SignOptions , SyncOptions } ;
14
+ use bdk:: { FeeRate , SignOptions , SyncOptions } ;
14
15
15
- use bitcoin:: { BlockHash , Script , Transaction , Txid } ;
16
+ use bitcoin:: { Script , Transaction , Txid } ;
16
17
17
18
use std:: collections:: HashSet ;
18
- use std:: sync:: { Arc , Mutex } ;
19
+ use std:: sync:: { mpsc , Arc , Mutex , RwLock } ;
19
20
20
21
/// The minimum feerate we are allowed to send, as specify by LDK.
21
22
const MIN_FEERATE : u32 = 253 ;
22
23
24
+ // The used 'stop gap' parameter used by BDK's wallet sync. This seems to configure the threshold
25
+ // number of blocks after which BDK stops looking for scripts belonging to the wallet.
26
+ const BDK_CLIENT_STOP_GAP : usize = 20 ;
27
+
28
+ // The number of concurrent requests made against the API provider.
29
+ const BDK_CLIENT_CONCURRENCY : u8 = 8 ;
30
+
23
31
pub struct ChainAccess < D >
24
32
where
25
33
D : BatchDatabase ,
26
34
{
27
- blockchain : EsploraBlockchain ,
35
+ blockchain : Arc < EsploraBlockchain > ,
36
+ client : Arc < esplora_client:: AsyncClient > ,
28
37
wallet : Mutex < bdk:: Wallet < D > > ,
29
38
queued_transactions : Mutex < Vec < Txid > > ,
30
39
watched_transactions : Mutex < Vec < Txid > > ,
31
40
queued_outputs : Mutex < Vec < WatchedOutput > > ,
32
41
watched_outputs : Mutex < Vec < WatchedOutput > > ,
33
42
last_sync_height : tokio:: sync:: Mutex < Option < u32 > > ,
43
+ tokio_runtime : RwLock < Option < Arc < tokio:: runtime:: Runtime > > > ,
44
+ _config : Arc < Config > ,
34
45
logger : Arc < FilesystemLogger > ,
35
46
}
36
47
@@ -39,38 +50,57 @@ where
39
50
D : BatchDatabase ,
40
51
{
41
52
pub ( crate ) fn new (
42
- blockchain : EsploraBlockchain , wallet : bdk:: Wallet < D > , logger : Arc < FilesystemLogger > ,
53
+ wallet : bdk:: Wallet < D > , config : Arc < Config > , logger : Arc < FilesystemLogger > ,
43
54
) -> Self {
44
55
let wallet = Mutex :: new ( wallet) ;
45
56
let watched_transactions = Mutex :: new ( Vec :: new ( ) ) ;
46
57
let queued_transactions = Mutex :: new ( Vec :: new ( ) ) ;
47
58
let watched_outputs = Mutex :: new ( Vec :: new ( ) ) ;
48
59
let queued_outputs = Mutex :: new ( Vec :: new ( ) ) ;
49
60
let last_sync_height = tokio:: sync:: Mutex :: new ( None ) ;
61
+ let tokio_runtime = RwLock :: new ( None ) ;
62
+ // TODO: Check that we can be sure that the Esplora client re-connects in case of failure
63
+ // and and exits cleanly on drop. Otherwise we need to handle this/move it to the runtime?
64
+ let blockchain = Arc :: new (
65
+ EsploraBlockchain :: new ( & config. esplora_server_url , BDK_CLIENT_STOP_GAP )
66
+ . with_concurrency ( BDK_CLIENT_CONCURRENCY ) ,
67
+ ) ;
68
+ let client_builder =
69
+ esplora_client:: Builder :: new ( & format ! ( "http://{}" , & config. esplora_server_url) ) ;
70
+ let client = Arc :: new ( client_builder. build_async ( ) . unwrap ( ) ) ;
50
71
Self {
51
72
blockchain,
73
+ client,
52
74
wallet,
53
75
queued_transactions,
54
76
watched_transactions,
55
77
queued_outputs,
56
78
watched_outputs,
57
79
last_sync_height,
80
+ tokio_runtime,
81
+ _config : config,
58
82
logger,
59
83
}
60
84
}
61
85
86
+ pub ( crate ) fn set_runtime ( & self , tokio_runtime : Arc < tokio:: runtime:: Runtime > ) {
87
+ * self . tokio_runtime . write ( ) . unwrap ( ) = Some ( tokio_runtime) ;
88
+ }
89
+
90
+ pub ( crate ) fn drop_runtime ( & self ) {
91
+ * self . tokio_runtime . write ( ) . unwrap ( ) = None ;
92
+ }
93
+
62
94
pub ( crate ) async fn sync_wallet ( & self ) -> Result < ( ) , Error > {
63
95
let sync_options = SyncOptions { progress : None } ;
64
96
65
- self . wallet . lock ( ) . unwrap ( ) . sync ( & self . blockchain , sync_options) ?;
97
+ self . wallet . lock ( ) . unwrap ( ) . sync ( & self . blockchain , sync_options) . await ?;
66
98
67
99
Ok ( ( ) )
68
100
}
69
101
70
- pub ( crate ) async fn sync ( & self , confirmables : Vec < & ( dyn Confirm + Sync ) > ) -> Result < ( ) , Error > {
71
- let client = & * self . blockchain ;
72
-
73
- let cur_height = client. get_height ( ) . await ?;
102
+ pub ( crate ) async fn sync ( & self , confirmables : Vec < & ( dyn Confirm + Send + Sync ) > ) -> Result < ( ) , Error > {
103
+ let cur_height = self . client . get_height ( ) . await ?;
74
104
75
105
let mut locked_last_sync_height = self . last_sync_height . lock ( ) . await ;
76
106
if cur_height >= locked_last_sync_height. unwrap_or ( 0 ) {
@@ -84,13 +114,11 @@ where
84
114
}
85
115
86
116
async fn sync_best_block_updated (
87
- & self , confirmables : & Vec < & ( dyn Confirm + Sync ) > , cur_height : u32 ,
117
+ & self , confirmables : & Vec < & ( dyn Confirm + Send + Sync ) > , cur_height : u32 ,
88
118
locked_last_sync_height : & mut tokio:: sync:: MutexGuard < ' _ , Option < u32 > > ,
89
119
) -> Result < ( ) , Error > {
90
- let client = & * self . blockchain ;
91
-
92
120
// Inform the interface of the new block.
93
- let cur_block_header = client. get_header ( cur_height) . await ?;
121
+ let cur_block_header = self . client . get_header ( cur_height) . await ?;
94
122
for c in confirmables {
95
123
c. best_block_updated ( & cur_block_header, cur_height) ;
96
124
}
@@ -100,10 +128,8 @@ where
100
128
}
101
129
102
130
async fn sync_transactions_confirmed (
103
- & self , confirmables : & Vec < & ( dyn Confirm + Sync ) > ,
131
+ & self , confirmables : & Vec < & ( dyn Confirm + Send + Sync ) > ,
104
132
) -> Result < ( ) , Error > {
105
- let client = & * self . blockchain ;
106
-
107
133
// First, check the confirmation status of registered transactions as well as the
108
134
// status of dependent transactions of registered outputs.
109
135
@@ -125,13 +151,13 @@ where
125
151
let mut unconfirmed_registered_txs = Vec :: new ( ) ;
126
152
127
153
for txid in registered_txs {
128
- if let Some ( tx_status) = client. get_tx_status ( & txid) . await ? {
154
+ if let Some ( tx_status) = self . client . get_tx_status ( & txid) . await ? {
129
155
if tx_status. confirmed {
130
- if let Some ( tx) = client. get_tx ( & txid) . await ? {
156
+ if let Some ( tx) = self . client . get_tx ( & txid) . await ? {
131
157
if let Some ( block_height) = tx_status. block_height {
132
158
// TODO: Switch to `get_header_by_hash` once released upstream (https://github.com/bitcoindevkit/rust-esplora-client/pull/17)
133
- let block_header = client. get_header ( block_height) . await ?;
134
- if let Some ( merkle_proof) = client. get_merkle_proof ( & txid) . await ? {
159
+ let block_header = self . client . get_header ( block_height) . await ?;
160
+ if let Some ( merkle_proof) = self . client . get_merkle_proof ( & txid) . await ? {
135
161
if block_height == merkle_proof. block_height {
136
162
confirmed_txs. push ( (
137
163
tx,
@@ -160,20 +186,21 @@ where
160
186
let mut unspent_registered_outputs = Vec :: new ( ) ;
161
187
162
188
for output in registered_outputs {
163
- if let Some ( output_status) = client
189
+ if let Some ( output_status) = self
190
+ . client
164
191
. get_output_status ( & output. outpoint . txid , output. outpoint . index as u64 )
165
192
. await ?
166
193
{
167
194
if output_status. spent {
168
195
if let Some ( spending_tx_status) = output_status. status {
169
196
if spending_tx_status. confirmed {
170
197
let spending_txid = output_status. txid . unwrap ( ) ;
171
- if let Some ( spending_tx) = client. get_tx ( & spending_txid) . await ? {
198
+ if let Some ( spending_tx) = self . client . get_tx ( & spending_txid) . await ? {
172
199
let block_height = spending_tx_status. block_height . unwrap ( ) ;
173
200
// TODO: Switch to `get_header_by_hash` once released upstream (https://github.com/bitcoindevkit/rust-esplora-client/pull/17)
174
- let block_header = client. get_header ( block_height) . await ?;
201
+ let block_header = self . client . get_header ( block_height) . await ?;
175
202
if let Some ( merkle_proof) =
176
- client. get_merkle_proof ( & spending_txid) . await ?
203
+ self . client . get_merkle_proof ( & spending_txid) . await ?
177
204
{
178
205
confirmed_txs. push ( (
179
206
spending_tx,
@@ -213,15 +240,15 @@ where
213
240
}
214
241
215
242
async fn sync_transaction_unconfirmed (
216
- & self , confirmables : & Vec < & ( dyn Confirm + Sync ) > ,
243
+ & self , confirmables : & Vec < & ( dyn Confirm + Send + Sync ) > ,
217
244
) -> Result < ( ) , Error > {
218
- let client = & * self . blockchain ;
219
245
// Query the interface for relevant txids and check whether they have been
220
246
// reorged-out of the chain.
221
247
let relevant_txids =
222
248
confirmables. iter ( ) . flat_map ( |c| c. get_relevant_txids ( ) ) . collect :: < HashSet < Txid > > ( ) ;
223
249
for txid in relevant_txids {
224
- let tx_unconfirmed = client
250
+ let tx_unconfirmed = self
251
+ . client
225
252
. get_tx_status ( & txid)
226
253
. await
227
254
. ok ( )
@@ -240,12 +267,14 @@ where
240
267
pub ( crate ) fn create_funding_transaction (
241
268
& self , output_script : & Script , value_sats : u64 , confirmation_target : ConfirmationTarget ,
242
269
) -> Result < Transaction , Error > {
243
- let num_blocks = num_blocks_from_conf_target ( confirmation_target) ;
244
- let fee_rate = self . blockchain . estimate_fee ( num_blocks) ?;
245
-
246
270
let locked_wallet = self . wallet . lock ( ) . unwrap ( ) ;
247
271
let mut tx_builder = locked_wallet. build_tx ( ) ;
248
272
273
+ let fallback_fee = fallback_fee_from_conf_target ( confirmation_target) ;
274
+ let fee_rate = self
275
+ . estimate_fee ( confirmation_target)
276
+ . unwrap_or ( FeeRate :: from_sat_per_kwu ( fallback_fee as f32 ) ) ;
277
+
249
278
tx_builder. add_recipient ( output_script. clone ( ) , value_sats) . fee_rate ( fee_rate) . enable_rbf ( ) ;
250
279
251
280
let ( mut psbt, _) = tx_builder. finish ( ) ?;
@@ -271,17 +300,43 @@ where
271
300
let address_info = self . wallet . lock ( ) . unwrap ( ) . get_address ( AddressIndex :: New ) ?;
272
301
Ok ( address_info. address )
273
302
}
303
+
304
+ fn estimate_fee ( & self , confirmation_target : ConfirmationTarget ) -> Result < bdk:: FeeRate , Error > {
305
+ let num_blocks = num_blocks_from_conf_target ( confirmation_target) ;
306
+
307
+ let locked_runtime = self . tokio_runtime . read ( ) . unwrap ( ) ;
308
+ if locked_runtime. as_ref ( ) . is_none ( ) {
309
+ return Err ( Error :: FeeEstimationFailed ) ;
310
+ }
311
+
312
+ let tokio_client = Arc :: clone ( & self . client ) ;
313
+ let ( sender, receiver) = mpsc:: sync_channel ( 1 ) ;
314
+
315
+ locked_runtime. as_ref ( ) . unwrap ( ) . spawn ( async move {
316
+ let res = tokio_client. get_fee_estimates ( ) . await ;
317
+ let _ = sender. send ( res) ;
318
+ } ) ;
319
+
320
+ let estimates = receiver
321
+ . recv ( )
322
+ . map_err ( |_| Error :: FeeEstimationFailed ) ?
323
+ . map_err ( |_| Error :: FeeEstimationFailed ) ?;
324
+
325
+ Ok ( bdk:: FeeRate :: from_sat_per_vb (
326
+ esplora_client:: convert_fee_rate ( num_blocks, estimates)
327
+ . map_err ( |_| Error :: FeeEstimationFailed ) ?,
328
+ ) )
329
+ }
274
330
}
275
331
276
332
impl < D > FeeEstimator for ChainAccess < D >
277
333
where
278
334
D : BatchDatabase ,
279
335
{
280
336
fn get_est_sat_per_1000_weight ( & self , confirmation_target : ConfirmationTarget ) -> u32 {
281
- let num_blocks = num_blocks_from_conf_target ( confirmation_target) ;
282
337
let fallback_fee = fallback_fee_from_conf_target ( confirmation_target) ;
283
- self . blockchain
284
- . estimate_fee ( num_blocks )
338
+
339
+ self . estimate_fee ( confirmation_target )
285
340
. map_or ( fallback_fee, |fee_rate| ( fee_rate. fee_wu ( 1000 ) as u32 ) . max ( MIN_FEERATE ) ) as u32
286
341
}
287
342
}
@@ -291,7 +346,22 @@ where
291
346
D : BatchDatabase ,
292
347
{
293
348
fn broadcast_transaction ( & self , tx : & Transaction ) {
294
- match self . blockchain . broadcast ( tx) {
349
+ let locked_runtime = self . tokio_runtime . read ( ) . unwrap ( ) ;
350
+ if locked_runtime. as_ref ( ) . is_none ( ) {
351
+ log_error ! ( self . logger, "Failed to broadcast transaction: No runtime." ) ;
352
+ return ;
353
+ }
354
+
355
+ let tokio_client = Arc :: clone ( & self . client ) ;
356
+ let tokio_tx = tx. clone ( ) ;
357
+ let ( sender, receiver) = mpsc:: sync_channel ( 1 ) ;
358
+
359
+ locked_runtime. as_ref ( ) . unwrap ( ) . spawn ( async move {
360
+ let res = tokio_client. broadcast ( & tokio_tx) . await ;
361
+ let _ = sender. send ( res) ;
362
+ } ) ;
363
+
364
+ match receiver. recv ( ) . unwrap ( ) {
295
365
Ok ( _) => { }
296
366
Err ( err) => {
297
367
log_error ! ( self . logger, "Failed to broadcast transaction: {}" , err) ;
@@ -315,33 +385,6 @@ where
315
385
}
316
386
}
317
387
318
- impl < D > GetHeight for ChainAccess < D >
319
- where
320
- D : BatchDatabase ,
321
- {
322
- fn get_height ( & self ) -> Result < u32 , bdk:: Error > {
323
- self . blockchain . get_height ( )
324
- }
325
- }
326
-
327
- impl < D > GetBlockHash for ChainAccess < D >
328
- where
329
- D : BatchDatabase ,
330
- {
331
- fn get_block_hash ( & self , height : u64 ) -> Result < BlockHash , bdk:: Error > {
332
- self . blockchain . get_block_hash ( height)
333
- }
334
- }
335
-
336
- impl < D > GetTx for ChainAccess < D >
337
- where
338
- D : BatchDatabase ,
339
- {
340
- fn get_tx ( & self , txid : & Txid ) -> Result < Option < Transaction > , bdk:: Error > {
341
- self . blockchain . get_tx ( txid)
342
- }
343
- }
344
-
345
388
fn num_blocks_from_conf_target ( confirmation_target : ConfirmationTarget ) -> usize {
346
389
match confirmation_target {
347
390
ConfirmationTarget :: Background => 12 ,
0 commit comments