Skip to content

Commit 969b408

Browse files
committed
Implement update_fee_rate_estimates for ChainSource::Electrum
1 parent b166833 commit 969b408

File tree

2 files changed

+146
-8
lines changed

2 files changed

+146
-8
lines changed

src/chain/electrum.rs

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
66
// accordance with one or both of these licenses.
77

8-
use crate::config::TX_BROADCAST_TIMEOUT_SECS;
8+
use crate::config::{Config, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS};
99
use crate::error::Error;
10+
use crate::fee_estimator::{
11+
apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target,
12+
ConfirmationTarget,
13+
};
1014
use crate::logger::{log_bytes, log_error, log_trace, LdkLogger, Logger};
1115

1216
use lightning::chain::{Filter, WatchedOutput};
@@ -15,10 +19,11 @@ use lightning_transaction_sync::ElectrumSyncClient;
1519

1620
use bdk_electrum::BdkElectrumClient;
1721

18-
use electrum_client::{Client as ElectrumClient, ElectrumApi};
22+
use electrum_client::{Batch, Client as ElectrumClient, ElectrumApi};
1923

20-
use bitcoin::{Script, Transaction, Txid};
24+
use bitcoin::{FeeRate, Network, Script, Transaction, Txid};
2125

26+
use std::collections::HashMap;
2227
use std::sync::Arc;
2328
use std::time::Duration;
2429

@@ -27,12 +32,14 @@ pub(crate) struct ElectrumRuntimeClient {
2732
bdk_electrum_client: Arc<BdkElectrumClient<ElectrumClient>>,
2833
tx_sync: Arc<ElectrumSyncClient<Arc<Logger>>>,
2934
runtime: Arc<tokio::runtime::Runtime>,
35+
config: Arc<Config>,
3036
logger: Arc<Logger>,
3137
}
3238

3339
impl ElectrumRuntimeClient {
3440
pub(crate) fn new(
35-
server_url: String, runtime: Arc<tokio::runtime::Runtime>, logger: Arc<Logger>,
41+
server_url: String, runtime: Arc<tokio::runtime::Runtime>, config: Arc<Config>,
42+
logger: Arc<Logger>,
3643
) -> Result<Self, Error> {
3744
let electrum_client = Arc::new(ElectrumClient::new(&server_url).map_err(|e| {
3845
log_error!(logger, "Failed to connect to electrum server: {}", e);
@@ -49,7 +56,7 @@ impl ElectrumRuntimeClient {
4956
Error::ConnectionFailed
5057
})?,
5158
);
52-
Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, logger })
59+
Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, config, logger })
5360
}
5461

5562
pub(crate) async fn broadcast(&self, tx: Transaction) {
@@ -93,6 +100,89 @@ impl ElectrumRuntimeClient {
93100
},
94101
}
95102
}
103+
104+
pub(crate) async fn get_fee_rate_cache_update(
105+
&self,
106+
) -> Result<HashMap<ConfirmationTarget, FeeRate>, Error> {
107+
let electrum_client = Arc::clone(&self.electrum_client);
108+
109+
let mut batch = Batch::default();
110+
let confirmation_targets = get_all_conf_targets();
111+
for target in confirmation_targets {
112+
let num_blocks = get_num_block_defaults_for_target(target);
113+
batch.estimate_fee(num_blocks);
114+
}
115+
116+
let spawn_fut = self.runtime.spawn_blocking(move || electrum_client.batch_call(&batch));
117+
118+
let timeout_fut = tokio::time::timeout(
119+
Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS),
120+
spawn_fut,
121+
);
122+
123+
let raw_estimates_btc_kvb = timeout_fut
124+
.await
125+
.map_err(|e| {
126+
log_error!(self.logger, "Updating fee rate estimates timed out: {}", e);
127+
Error::FeerateEstimationUpdateTimeout
128+
})?
129+
.map_err(|e| {
130+
log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e);
131+
Error::FeerateEstimationUpdateFailed
132+
})?
133+
.map_err(|e| {
134+
log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e);
135+
Error::FeerateEstimationUpdateFailed
136+
})?;
137+
138+
if raw_estimates_btc_kvb.len() != confirmation_targets.len()
139+
&& self.config.network == Network::Bitcoin
140+
{
141+
// Ensure we fail if we didn't receive all estimates.
142+
debug_assert!(false,
143+
"Electrum server didn't return all expected results. This is disallowed on Mainnet."
144+
);
145+
log_error!(self.logger,
146+
"Failed to retrieve fee rate estimates: Electrum server didn't return all expected results. This is disallowed on Mainnet."
147+
);
148+
return Err(Error::FeerateEstimationUpdateFailed);
149+
}
150+
151+
let mut new_fee_rate_cache = HashMap::with_capacity(10);
152+
for (target, raw_fee_rate_btc_per_kvb) in
153+
confirmation_targets.into_iter().zip(raw_estimates_btc_kvb.into_iter())
154+
{
155+
// Parse the retrieved serde_json::Value and fall back to 1 sat/vb (10^3 / 10^8 = 10^-5
156+
// = 0.00001 btc/kvb) if we fail or it yields less than that. This is mostly necessary
157+
// to continue on `signet`/`regtest` where we might not get estimates (or bogus
158+
// values).
159+
let fee_rate_btc_per_kvb = raw_fee_rate_btc_per_kvb
160+
.as_f64()
161+
.map_or(0.00001, |converted| converted.max(0.00001));
162+
163+
// Electrum, just like Bitcoin Core, gives us a feerate in BTC/KvB.
164+
// Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu.
165+
let fee_rate = {
166+
let fee_rate_sat_per_kwu = (fee_rate_btc_per_kvb * 25_000_000.0).round() as u64;
167+
FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu)
168+
};
169+
170+
// LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that
171+
// require some post-estimation adjustments to the fee rates, which we do here.
172+
let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate);
173+
174+
new_fee_rate_cache.insert(target, adjusted_fee_rate);
175+
176+
log_trace!(
177+
self.logger,
178+
"Fee rate estimation updated for {:?}: {} sats/kwu",
179+
target,
180+
adjusted_fee_rate.to_sat_per_kwu(),
181+
);
182+
}
183+
184+
Ok(new_fee_rate_cache)
185+
}
96186
}
97187

98188
impl Filter for ElectrumRuntimeClient {

src/chain/mod.rs

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,17 @@ impl ChainSource {
247247
electrum_runtime_client,
248248
registered_txs_cache,
249249
registered_outputs_cache,
250+
config,
250251
logger,
251252
..
252253
} => {
253254
let mut locked_client = electrum_runtime_client.write().unwrap();
254-
let client =
255-
ElectrumRuntimeClient::new(server_url.clone(), runtime, Arc::clone(&logger))?;
255+
let client = ElectrumRuntimeClient::new(
256+
server_url.clone(),
257+
runtime,
258+
Arc::clone(&config),
259+
Arc::clone(&logger),
260+
)?;
256261

257262
// Apply any previously-cached `Filter` entries
258263
{
@@ -979,7 +984,50 @@ impl ChainSource {
979984

980985
Ok(())
981986
},
982-
Self::Electrum { .. } => todo!(),
987+
Self::Electrum {
988+
electrum_runtime_client,
989+
fee_estimator,
990+
kv_store,
991+
logger,
992+
node_metrics,
993+
..
994+
} => {
995+
let electrum_client: Arc<ElectrumRuntimeClient> =
996+
if let Some(client) = electrum_runtime_client.read().unwrap().as_ref() {
997+
Arc::clone(client)
998+
} else {
999+
debug_assert!(
1000+
false,
1001+
"We should have started the chain source before updating fees"
1002+
);
1003+
return Err(Error::FeerateEstimationUpdateFailed);
1004+
};
1005+
1006+
let now = Instant::now();
1007+
1008+
let new_fee_rate_cache = electrum_client.get_fee_rate_cache_update().await?;
1009+
fee_estimator.set_fee_rate_cache(new_fee_rate_cache);
1010+
1011+
log_info!(
1012+
logger,
1013+
"Fee rate cache update finished in {}ms.",
1014+
now.elapsed().as_millis()
1015+
);
1016+
1017+
let unix_time_secs_opt =
1018+
SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs());
1019+
{
1020+
let mut locked_node_metrics = node_metrics.write().unwrap();
1021+
locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt;
1022+
write_node_metrics(
1023+
&*locked_node_metrics,
1024+
Arc::clone(&kv_store),
1025+
Arc::clone(&logger),
1026+
)?;
1027+
}
1028+
1029+
Ok(())
1030+
},
9831031
Self::BitcoindRpc {
9841032
bitcoind_rpc_client,
9851033
fee_estimator,

0 commit comments

Comments
 (0)