Skip to content

Commit 1ffb525

Browse files
committed
Implement fee rate estmation updating
1 parent 12adcb9 commit 1ffb525

File tree

4 files changed

+203
-6
lines changed

4 files changed

+203
-6
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thr
7373
esplora-client = { version = "0.9", default-features = false }
7474
libc = "0.2"
7575
uniffi = { version = "0.26.0", features = ["build"], optional = true }
76+
serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] }
7677
serde_json = { version = "1.0.128", default-features = false, features = ["std"] }
7778

7879
[target.'cfg(vss)'.dependencies]

src/chain/bitcoind_rpc.rs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ use crate::types::{ChainMonitor, ChannelManager, Sweeper, Wallet};
1010
use lightning::chain::Listen;
1111

1212
use lightning_block_sync::http::HttpEndpoint;
13+
use lightning_block_sync::http::JsonResponse;
1314
use lightning_block_sync::poll::ValidatedBlockHeader;
1415
use lightning_block_sync::rpc::RpcClient;
1516
use lightning_block_sync::{
1617
AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, Cache,
1718
};
1819

19-
use bitcoin::{BlockHash, Transaction, Txid};
20+
use serde::Serialize;
21+
22+
use bitcoin::{BlockHash, FeeRate, Transaction, Txid};
2023

2124
use base64::prelude::{Engine, BASE64_STANDARD};
2225

@@ -46,6 +49,27 @@ impl BitcoindRpcClient {
4649
let tx_json = serde_json::json!(tx_serialized);
4750
self.rpc_client.call_method::<Txid>("sendrawtransaction", &vec![tx_json]).await
4851
}
52+
53+
pub(crate) async fn get_fee_estimate_for_target(
54+
&self, num_blocks: usize, estimation_mode: FeeRateEstimationMode,
55+
) -> std::io::Result<FeeRate> {
56+
let num_blocks_json = serde_json::json!(num_blocks);
57+
let estimation_mode_json = serde_json::json!(estimation_mode);
58+
self.rpc_client
59+
.call_method::<FeeResponse>(
60+
"estimatesmartfee",
61+
&vec![num_blocks_json, estimation_mode_json],
62+
)
63+
.await
64+
.map(|resp| resp.0)
65+
}
66+
67+
pub(crate) async fn get_mempool_minimum_fee_rate(&self) -> std::io::Result<FeeRate> {
68+
self.rpc_client
69+
.call_method::<MempoolMinFeeResponse>("getmempoolinfo", &vec![])
70+
.await
71+
.map(|resp| resp.0)
72+
}
4973
}
5074

5175
impl BlockSource for BitcoindRpcClient {
@@ -66,6 +90,55 @@ impl BlockSource for BitcoindRpcClient {
6690
}
6791
}
6892

93+
pub(crate) struct FeeResponse(pub FeeRate);
94+
95+
impl TryInto<FeeResponse> for JsonResponse {
96+
type Error = std::io::Error;
97+
fn try_into(self) -> std::io::Result<FeeResponse> {
98+
if !self.0["errors"].is_null() {
99+
return Err(std::io::Error::new(
100+
std::io::ErrorKind::Other,
101+
self.0["errors"].to_string(),
102+
));
103+
}
104+
let fee_rate_btc_per_kvbyte = self.0["feerate"]
105+
.as_f64()
106+
.ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse fee rate"))?;
107+
// Bitcoin Core gives us a feerate in BTC/KvB.
108+
// Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu.
109+
let fee_rate = {
110+
let fee_rate_sat_per_kwu = (fee_rate_btc_per_kvbyte * 25_000_000.0).round() as u64;
111+
FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu)
112+
};
113+
Ok(FeeResponse(fee_rate))
114+
}
115+
}
116+
117+
pub struct MempoolMinFeeResponse(pub FeeRate);
118+
119+
impl TryInto<MempoolMinFeeResponse> for JsonResponse {
120+
type Error = std::io::Error;
121+
fn try_into(self) -> std::io::Result<MempoolMinFeeResponse> {
122+
let fee_rate_btc_per_kvbyte = self.0["mempoolminfee"]
123+
.as_f64()
124+
.ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse fee rate"))?;
125+
// Bitcoin Core gives us a feerate in BTC/KvB.
126+
// Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu.
127+
let fee_rate = {
128+
let fee_rate_sat_per_kwu = (fee_rate_btc_per_kvbyte * 25_000_000.0).round() as u64;
129+
FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu)
130+
};
131+
Ok(MempoolMinFeeResponse(fee_rate))
132+
}
133+
}
134+
135+
#[derive(Debug, Clone, Serialize)]
136+
#[serde(rename_all = "UPPERCASE")]
137+
pub(crate) enum FeeRateEstimationMode {
138+
Economical,
139+
Conservative,
140+
}
141+
69142
const MAX_HEADER_CACHE_ENTRIES: usize = 100;
70143

71144
pub(crate) struct BoundedHeaderCache {

src/chain/mod.rs

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
mod bitcoind_rpc;
99

10-
use crate::chain::bitcoind_rpc::{BitcoindRpcClient, BoundedHeaderCache, ChainListener};
10+
use crate::chain::bitcoind_rpc::{
11+
BitcoindRpcClient, BoundedHeaderCache, ChainListener, FeeRateEstimationMode,
12+
};
1113
use crate::config::{
1214
Config, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP,
1315
BDK_WALLET_SYNC_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS,
@@ -16,13 +18,14 @@ use crate::config::{
1618
};
1719
use crate::fee_estimator::{
1820
apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target,
19-
OnchainFeeEstimator,
21+
ConfirmationTarget, OnchainFeeEstimator,
2022
};
2123
use crate::io::utils::write_node_metrics;
2224
use crate::logger::{log_bytes, log_error, log_info, log_trace, FilesystemLogger, Logger};
2325
use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet};
2426
use crate::{Error, NodeMetrics};
2527

28+
use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget;
2629
use lightning::chain::{Confirm, Filter, Listen};
2730
use lightning::util::ser::Writeable;
2831

@@ -356,6 +359,13 @@ impl ChainSource {
356359
chain_polling_interval
357360
.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
358361

362+
let mut fee_rate_update_interval =
363+
tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS));
364+
// When starting up, we just blocked on updating, so skip the first tick.
365+
fee_rate_update_interval.reset();
366+
fee_rate_update_interval
367+
.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
368+
359369
// Start the polling loop.
360370
loop {
361371
tokio::select! {
@@ -369,6 +379,9 @@ impl ChainSource {
369379
_ = chain_polling_interval.tick() => {
370380
let _ = self.poll_and_update_listeners(Arc::clone(&channel_manager), Arc::clone(&chain_monitor), Arc::clone(&output_sweeper)).await;
371381
}
382+
_ = fee_rate_update_interval.tick() => {
383+
let _ = self.update_fee_rate_estimates().await;
384+
}
372385
}
373386
}
374387
},
@@ -805,7 +818,108 @@ impl ChainSource {
805818

806819
Ok(())
807820
},
808-
Self::BitcoindRpc { .. } => todo!(),
821+
Self::BitcoindRpc {
822+
bitcoind_rpc_client,
823+
fee_estimator,
824+
kv_store,
825+
logger,
826+
node_metrics,
827+
..
828+
} => {
829+
macro_rules! get_fee_rate_update {
830+
($estimation_fut: expr) => {{
831+
tokio::time::timeout(
832+
Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS),
833+
$estimation_fut,
834+
)
835+
.await
836+
.map_err(|e| {
837+
log_error!(logger, "Updating fee rate estimates timed out: {}", e);
838+
Error::FeerateEstimationUpdateTimeout
839+
})?
840+
.map_err(|e| {
841+
log_error!(logger, "Failed to retrieve fee rate estimates: {}", e);
842+
Error::FeerateEstimationUpdateFailed
843+
})?
844+
}};
845+
}
846+
let confirmation_targets = get_all_conf_targets();
847+
848+
let mut new_fee_rate_cache = HashMap::with_capacity(10);
849+
let now = Instant::now();
850+
for target in confirmation_targets {
851+
let fee_rate = match target {
852+
ConfirmationTarget::Lightning(
853+
LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee,
854+
) => {
855+
let estimation_fut = bitcoind_rpc_client.get_mempool_minimum_fee_rate();
856+
get_fee_rate_update!(estimation_fut)
857+
},
858+
ConfirmationTarget::Lightning(
859+
LdkConfirmationTarget::MaximumFeeEstimate,
860+
) => {
861+
let num_blocks = get_num_block_defaults_for_target(target);
862+
let estimation_mode = FeeRateEstimationMode::Conservative;
863+
let estimation_fut = bitcoind_rpc_client
864+
.get_fee_estimate_for_target(num_blocks, estimation_mode);
865+
get_fee_rate_update!(estimation_fut)
866+
},
867+
ConfirmationTarget::Lightning(
868+
LdkConfirmationTarget::UrgentOnChainSweep,
869+
) => {
870+
let num_blocks = get_num_block_defaults_for_target(target);
871+
let estimation_mode = FeeRateEstimationMode::Conservative;
872+
let estimation_fut = bitcoind_rpc_client
873+
.get_fee_estimate_for_target(num_blocks, estimation_mode);
874+
get_fee_rate_update!(estimation_fut)
875+
},
876+
_ => {
877+
// Otherwise, we default to economical block-target estimate.
878+
let num_blocks = get_num_block_defaults_for_target(target);
879+
let estimation_mode = FeeRateEstimationMode::Economical;
880+
let estimation_fut = bitcoind_rpc_client
881+
.get_fee_estimate_for_target(num_blocks, estimation_mode);
882+
get_fee_rate_update!(estimation_fut)
883+
},
884+
};
885+
886+
// LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that
887+
// require some post-estimation adjustments to the fee rates, which we do here.
888+
let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate);
889+
890+
new_fee_rate_cache.insert(target, adjusted_fee_rate);
891+
892+
log_trace!(
893+
logger,
894+
"Fee rate estimation updated for {:?}: {} sats/kwu",
895+
target,
896+
adjusted_fee_rate.to_sat_per_kwu(),
897+
);
898+
}
899+
900+
if fee_estimator.set_fee_rate_cache(new_fee_rate_cache) {
901+
// We only log if the values changed, as it might be very spammy otherwise.
902+
log_info!(
903+
logger,
904+
"Fee rate cache update finished in {}ms.",
905+
now.elapsed().as_millis()
906+
);
907+
}
908+
909+
let unix_time_secs_opt =
910+
SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs());
911+
{
912+
let mut locked_node_metrics = node_metrics.write().unwrap();
913+
locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt;
914+
write_node_metrics(
915+
&*locked_node_metrics,
916+
Arc::clone(&kv_store),
917+
Arc::clone(&logger),
918+
)?;
919+
}
920+
921+
Ok(())
922+
},
809923
}
810924
}
811925

src/fee_estimator.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,17 @@ impl OnchainFeeEstimator {
4444
Self { fee_rate_cache }
4545
}
4646

47-
pub(crate) fn set_fee_rate_cache(&self, fee_rate_cache: HashMap<ConfirmationTarget, FeeRate>) {
48-
*self.fee_rate_cache.write().unwrap() = fee_rate_cache;
47+
// Updates the fee rate cache and returns if the new values changed.
48+
pub(crate) fn set_fee_rate_cache(
49+
&self, fee_rate_cache_update: HashMap<ConfirmationTarget, FeeRate>,
50+
) -> bool {
51+
let mut locked_fee_rate_cache = self.fee_rate_cache.write().unwrap();
52+
if fee_rate_cache_update != *locked_fee_rate_cache {
53+
*locked_fee_rate_cache = fee_rate_cache_update;
54+
true
55+
} else {
56+
false
57+
}
4958
}
5059
}
5160

0 commit comments

Comments
 (0)