Skip to content

Commit ee164a1

Browse files
committed
Implement fee rate estmation updating
1 parent 01a7b6c commit ee164a1

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(super) 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

@@ -347,6 +350,13 @@ impl ChainSource {
347350
chain_polling_interval
348351
.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
349352

353+
let mut fee_rate_update_interval =
354+
tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS));
355+
// When starting up, we just blocked on updating, so skip the first tick.
356+
fee_rate_update_interval.reset();
357+
fee_rate_update_interval
358+
.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
359+
350360
// Start the polling loop.
351361
loop {
352362
tokio::select! {
@@ -381,6 +391,9 @@ impl ChainSource {
381391
});
382392
}
383393
}
394+
_ = fee_rate_update_interval.tick() => {
395+
let _ = self.update_fee_rate_estimates().await;
396+
}
384397
}
385398
}
386399
},
@@ -703,7 +716,108 @@ impl ChainSource {
703716

704717
Ok(())
705718
},
706-
Self::BitcoindRpc { .. } => todo!(),
719+
Self::BitcoindRpc {
720+
bitcoind_rpc_client,
721+
fee_estimator,
722+
kv_store,
723+
logger,
724+
node_metrics,
725+
..
726+
} => {
727+
macro_rules! get_fee_rate_update {
728+
($estimation_fut: expr) => {{
729+
tokio::time::timeout(
730+
Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS),
731+
$estimation_fut,
732+
)
733+
.await
734+
.map_err(|e| {
735+
log_error!(logger, "Updating fee rate estimates timed out: {}", e);
736+
Error::FeerateEstimationUpdateTimeout
737+
})?
738+
.map_err(|e| {
739+
log_error!(logger, "Failed to retrieve fee rate estimates: {}", e);
740+
Error::FeerateEstimationUpdateFailed
741+
})?
742+
}};
743+
}
744+
let confirmation_targets = get_all_conf_targets();
745+
746+
let mut new_fee_rate_cache = HashMap::with_capacity(10);
747+
let now = Instant::now();
748+
for target in confirmation_targets {
749+
let fee_rate = match target {
750+
ConfirmationTarget::Lightning(
751+
LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee,
752+
) => {
753+
let estimation_fut = bitcoind_rpc_client.get_mempool_minimum_fee_rate();
754+
get_fee_rate_update!(estimation_fut)
755+
},
756+
ConfirmationTarget::Lightning(
757+
LdkConfirmationTarget::MaximumFeeEstimate,
758+
) => {
759+
let num_blocks = get_num_block_defaults_for_target(target);
760+
let estimation_mode = FeeRateEstimationMode::Conservative;
761+
let estimation_fut = bitcoind_rpc_client
762+
.get_fee_estimate_for_target(num_blocks, estimation_mode);
763+
get_fee_rate_update!(estimation_fut)
764+
},
765+
ConfirmationTarget::Lightning(
766+
LdkConfirmationTarget::UrgentOnChainSweep,
767+
) => {
768+
let num_blocks = get_num_block_defaults_for_target(target);
769+
let estimation_mode = FeeRateEstimationMode::Conservative;
770+
let estimation_fut = bitcoind_rpc_client
771+
.get_fee_estimate_for_target(num_blocks, estimation_mode);
772+
get_fee_rate_update!(estimation_fut)
773+
},
774+
_ => {
775+
// Otherwise, we default to economical block-target estimate.
776+
let num_blocks = get_num_block_defaults_for_target(target);
777+
let estimation_mode = FeeRateEstimationMode::Economical;
778+
let estimation_fut = bitcoind_rpc_client
779+
.get_fee_estimate_for_target(num_blocks, estimation_mode);
780+
get_fee_rate_update!(estimation_fut)
781+
},
782+
};
783+
784+
// LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that
785+
// require some post-estimation adjustments to the fee rates, which we do here.
786+
let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate);
787+
788+
new_fee_rate_cache.insert(target, adjusted_fee_rate);
789+
790+
log_trace!(
791+
logger,
792+
"Fee rate estimation updated for {:?}: {} sats/kwu",
793+
target,
794+
adjusted_fee_rate.to_sat_per_kwu(),
795+
);
796+
}
797+
798+
if fee_estimator.set_fee_rate_cache(new_fee_rate_cache) {
799+
// We only log if the values changed, as it might be very spammy otherwise.
800+
log_info!(
801+
logger,
802+
"Fee rate cache update finished in {}ms.",
803+
now.elapsed().as_millis()
804+
);
805+
}
806+
807+
let unix_time_secs_opt =
808+
SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs());
809+
{
810+
let mut locked_node_metrics = node_metrics.write().unwrap();
811+
locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt;
812+
write_node_metrics(
813+
&*locked_node_metrics,
814+
Arc::clone(&kv_store),
815+
Arc::clone(&logger),
816+
)?;
817+
}
818+
819+
Ok(())
820+
},
707821
}
708822
}
709823

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)