Skip to content

Commit 5462274

Browse files
committed
Implement fee rate estmation updating
1 parent 72f32b0 commit 5462274

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
@@ -8,7 +8,9 @@
88
mod bitcoind_rpc;
99
mod block_sync_init;
1010

11-
use crate::chain::bitcoind_rpc::{BitcoindRpcClient, BoundedHeaderCache, ChainListener};
11+
use crate::chain::bitcoind_rpc::{
12+
BitcoindRpcClient, BoundedHeaderCache, ChainListener, FeeRateEstimationMode,
13+
};
1214
use crate::chain::block_sync_init::synchronize_listeners;
1315
use crate::config::{
1416
Config, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP,
@@ -18,13 +20,14 @@ use crate::config::{
1820
};
1921
use crate::fee_estimator::{
2022
apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target,
21-
OnchainFeeEstimator,
23+
ConfirmationTarget, OnchainFeeEstimator,
2224
};
2325
use crate::io::utils::write_node_metrics;
2426
use crate::logger::{log_bytes, log_error, log_info, log_trace, FilesystemLogger, Logger};
2527
use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet};
2628
use crate::{Error, NodeMetrics};
2729

30+
use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget;
2831
use lightning::chain::{Confirm, Filter, Listen};
2932
use lightning::util::ser::Writeable;
3033

@@ -348,6 +351,13 @@ impl ChainSource {
348351
chain_polling_interval
349352
.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
350353

354+
let mut fee_rate_update_interval =
355+
tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS));
356+
// When starting up, we just blocked on updating, so skip the first tick.
357+
fee_rate_update_interval.reset();
358+
fee_rate_update_interval
359+
.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
360+
351361
// Start the polling loop.
352362
loop {
353363
tokio::select! {
@@ -382,6 +392,9 @@ impl ChainSource {
382392
});
383393
}
384394
}
395+
_ = fee_rate_update_interval.tick() => {
396+
let _ = self.update_fee_rate_estimates().await;
397+
}
385398
}
386399
}
387400
},
@@ -704,7 +717,108 @@ impl ChainSource {
704717

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

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)