Skip to content

Commit af64dd9

Browse files
committed
Implement update_fee_rate_estimates for ChainSource::Electrum
1 parent 98150cd commit af64dd9

File tree

2 files changed

+150
-10
lines changed

2 files changed

+150
-10
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};
@@ -17,10 +21,11 @@ use bdk_electrum::BdkElectrumClient;
1721

1822
use electrum_client::Client as ElectrumClient;
1923
use electrum_client::ConfigBuilder as ElectrumConfigBuilder;
20-
use electrum_client::ElectrumApi;
24+
use electrum_client::{Batch, ElectrumApi};
2125

22-
use bitcoin::{Script, Transaction, Txid};
26+
use bitcoin::{FeeRate, Network, Script, Transaction, Txid};
2327

28+
use std::collections::HashMap;
2429
use std::sync::Arc;
2530
use std::time::Duration;
2631

@@ -32,12 +37,14 @@ pub(crate) struct ElectrumRuntimeClient {
3237
bdk_electrum_client: Arc<BdkElectrumClient<ElectrumClient>>,
3338
tx_sync: Arc<ElectrumSyncClient<Arc<Logger>>>,
3439
runtime: Arc<tokio::runtime::Runtime>,
40+
config: Arc<Config>,
3541
logger: Arc<Logger>,
3642
}
3743

3844
impl ElectrumRuntimeClient {
3945
pub(crate) fn new(
40-
server_url: String, runtime: Arc<tokio::runtime::Runtime>, logger: Arc<Logger>,
46+
server_url: String, runtime: Arc<tokio::runtime::Runtime>, config: Arc<Config>,
47+
logger: Arc<Logger>,
4148
) -> Result<Self, Error> {
4249
let electrum_config = ElectrumConfigBuilder::new()
4350
.retry(ELECTRUM_CLIENT_NUM_RETRIES)
@@ -62,7 +69,7 @@ impl ElectrumRuntimeClient {
6269
Error::ConnectionFailed
6370
})?,
6471
);
65-
Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, logger })
72+
Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, config, logger })
6673
}
6774

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

111201
impl Filter for ElectrumRuntimeClient {

src/chain/mod.rs

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,17 @@ impl ElectrumRuntimeStatus {
125125
}
126126

127127
pub(crate) fn start(
128-
&mut self, server_url: String, runtime: Arc<tokio::runtime::Runtime>, logger: Arc<Logger>,
128+
&mut self, server_url: String, runtime: Arc<tokio::runtime::Runtime>, config: Arc<Config>,
129+
logger: Arc<Logger>,
129130
) -> Result<(), Error> {
130131
match self {
131132
Self::Stopped { pending_registered_txs, pending_registered_outputs } => {
132-
let client =
133-
Arc::new(ElectrumRuntimeClient::new(server_url.clone(), runtime, logger)?);
133+
let client = Arc::new(ElectrumRuntimeClient::new(
134+
server_url.clone(),
135+
runtime,
136+
config,
137+
logger,
138+
)?);
134139

135140
// Apply any pending `Filter` entries
136141
for (txid, script_pubkey) in pending_registered_txs.drain(..) {
@@ -307,10 +312,11 @@ impl ChainSource {
307312

308313
pub(crate) fn start(&self, runtime: Arc<tokio::runtime::Runtime>) -> Result<(), Error> {
309314
match self {
310-
Self::Electrum { server_url, electrum_runtime_status, logger, .. } => {
315+
Self::Electrum { server_url, electrum_runtime_status, config, logger, .. } => {
311316
electrum_runtime_status.write().unwrap().start(
312317
server_url.clone(),
313318
Arc::clone(&runtime),
319+
Arc::clone(&config),
314320
Arc::clone(&logger),
315321
)?;
316322
},
@@ -1055,7 +1061,51 @@ impl ChainSource {
10551061

10561062
Ok(())
10571063
},
1058-
Self::Electrum { .. } => todo!(),
1064+
Self::Electrum {
1065+
electrum_runtime_status,
1066+
fee_estimator,
1067+
kv_store,
1068+
logger,
1069+
node_metrics,
1070+
..
1071+
} => {
1072+
let electrum_client: Arc<ElectrumRuntimeClient> = if let Some(client) =
1073+
electrum_runtime_status.read().unwrap().client().as_ref()
1074+
{
1075+
Arc::clone(client)
1076+
} else {
1077+
debug_assert!(
1078+
false,
1079+
"We should have started the chain source before updating fees"
1080+
);
1081+
return Err(Error::FeerateEstimationUpdateFailed);
1082+
};
1083+
1084+
let now = Instant::now();
1085+
1086+
let new_fee_rate_cache = electrum_client.get_fee_rate_cache_update().await?;
1087+
fee_estimator.set_fee_rate_cache(new_fee_rate_cache);
1088+
1089+
log_info!(
1090+
logger,
1091+
"Fee rate cache update finished in {}ms.",
1092+
now.elapsed().as_millis()
1093+
);
1094+
1095+
let unix_time_secs_opt =
1096+
SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs());
1097+
{
1098+
let mut locked_node_metrics = node_metrics.write().unwrap();
1099+
locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt;
1100+
write_node_metrics(
1101+
&*locked_node_metrics,
1102+
Arc::clone(&kv_store),
1103+
Arc::clone(&logger),
1104+
)?;
1105+
}
1106+
1107+
Ok(())
1108+
},
10591109
Self::BitcoindRpc {
10601110
bitcoind_rpc_client,
10611111
fee_estimator,

0 commit comments

Comments
 (0)