Skip to content

Commit efea678

Browse files
committed
Introduce OnchainFeeEstimator
We also decouple fee estimation from the BDK on-chain wallet and BDK's corresponding `EsploraBlockchain`. Instead we use the esplora client directly in a dedicated `OnchainFeeEstimator` object. For one, this change is nice as it allows us to move more things out of the `Wallet` thread and corresponding locks. Moreover, it makes things more modular in general, which makes future upgrades and testing easier.
1 parent b31f451 commit efea678

File tree

8 files changed

+246
-157
lines changed

8 files changed

+246
-157
lines changed

bindings/ldk_node.udl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ enum NodeError {
108108
"ChannelClosingFailed",
109109
"ChannelConfigUpdateFailed",
110110
"PersistenceFailed",
111+
"FeerateEstimationUpdateFailed",
111112
"WalletOperationFailed",
112113
"OnchainTxSigningFailed",
113114
"MessageSigningFailed",

src/builder.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::event::EventQueue;
2+
use crate::fee_estimator::OnchainFeeEstimator;
23
use crate::gossip::GossipSource;
34
use crate::io;
45
use crate::io::sqlite_store::SqliteStore;
@@ -465,7 +466,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
465466
BuildError::WalletSetupFailed
466467
})?;
467468

468-
let (blockchain, tx_sync, tx_broadcaster) = match chain_data_source_config {
469+
let (blockchain, tx_sync, tx_broadcaster, fee_estimator) = match chain_data_source_config {
469470
Some(ChainDataSourceConfig::Esplora(server_url)) => {
470471
let tx_sync = Arc::new(EsploraSyncClient::new(server_url.clone(), Arc::clone(&logger)));
471472
let blockchain =
@@ -475,7 +476,9 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
475476
tx_sync.client().clone(),
476477
Arc::clone(&logger),
477478
));
478-
(blockchain, tx_sync, tx_broadcaster)
479+
let fee_estimator =
480+
Arc::new(OnchainFeeEstimator::new(tx_sync.client().clone(), Arc::clone(&logger)));
481+
(blockchain, tx_sync, tx_broadcaster, fee_estimator)
479482
}
480483
None => {
481484
// Default to Esplora client.
@@ -488,7 +491,9 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
488491
tx_sync.client().clone(),
489492
Arc::clone(&logger),
490493
));
491-
(blockchain, tx_sync, tx_broadcaster)
494+
let fee_estimator =
495+
Arc::new(OnchainFeeEstimator::new(tx_sync.client().clone(), Arc::clone(&logger)));
496+
(blockchain, tx_sync, tx_broadcaster, fee_estimator)
492497
}
493498
};
494499

@@ -497,6 +502,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
497502
blockchain,
498503
bdk_wallet,
499504
Arc::clone(&tx_broadcaster),
505+
Arc::clone(&fee_estimator),
500506
Arc::clone(&logger),
501507
));
502508

@@ -505,7 +511,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
505511
Some(Arc::clone(&tx_sync)),
506512
Arc::clone(&tx_broadcaster),
507513
Arc::clone(&logger),
508-
Arc::clone(&wallet),
514+
Arc::clone(&fee_estimator),
509515
Arc::clone(&kv_store),
510516
));
511517

@@ -605,7 +611,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
605611
Arc::clone(&keys_manager),
606612
Arc::clone(&keys_manager),
607613
Arc::clone(&keys_manager),
608-
Arc::clone(&wallet),
614+
Arc::clone(&fee_estimator),
609615
Arc::clone(&chain_monitor),
610616
Arc::clone(&tx_broadcaster),
611617
Arc::clone(&router),
@@ -629,7 +635,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
629635
best_block: BestBlock::new(genesis_block_hash, 0),
630636
};
631637
channelmanager::ChannelManager::new(
632-
Arc::clone(&wallet),
638+
Arc::clone(&fee_estimator),
633639
Arc::clone(&chain_monitor),
634640
Arc::clone(&tx_broadcaster),
635641
Arc::clone(&router),
@@ -781,6 +787,7 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
781787
wallet,
782788
tx_sync,
783789
tx_broadcaster,
790+
fee_estimator,
784791
event_queue,
785792
channel_manager,
786793
chain_monitor,

src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ pub enum Error {
2525
ChannelConfigUpdateFailed,
2626
/// Persistence failed.
2727
PersistenceFailed,
28+
/// A fee rate estimation update failed.
29+
FeerateEstimationUpdateFailed,
2830
/// A wallet operation failed.
2931
WalletOperationFailed,
3032
/// A signing operation for transaction failed.
@@ -79,6 +81,9 @@ impl fmt::Display for Error {
7981
Self::ChannelClosingFailed => write!(f, "Failed to close channel."),
8082
Self::ChannelConfigUpdateFailed => write!(f, "Failed to update channel config."),
8183
Self::PersistenceFailed => write!(f, "Failed to persist data."),
84+
Self::FeerateEstimationUpdateFailed => {
85+
write!(f, "Failed to update fee rate estimates.")
86+
}
8287
Self::WalletOperationFailed => write!(f, "Failed to conduct wallet operation."),
8388
Self::OnchainTxSigningFailed => write!(f, "Failed to sign given transaction."),
8489
Self::MessageSigningFailed => write!(f, "Failed to sign given message."),

src/event.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::types::{Broadcaster, Wallet};
1+
use crate::types::{Broadcaster, FeeEstimator, Wallet};
22
use crate::{hex_utils, ChannelManager, Config, Error, KeysManager, NetworkGraph, UserChannelId};
33

44
use crate::payment_store::{
@@ -11,7 +11,9 @@ use crate::io::{
1111
};
1212
use crate::logger::{log_debug, log_error, log_info, Logger};
1313

14-
use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator};
14+
use lightning::chain::chaininterface::{
15+
BroadcasterInterface, ConfirmationTarget, FeeEstimator as LDKFeeEstimator,
16+
};
1517
use lightning::events::Event as LdkEvent;
1618
use lightning::events::PaymentPurpose;
1719
use lightning::impl_writeable_tlv_based_enum;
@@ -245,6 +247,7 @@ where
245247
wallet: Arc<Wallet>,
246248
channel_manager: Arc<ChannelManager<K>>,
247249
tx_broadcaster: Arc<Broadcaster>,
250+
fee_estimator: Arc<FeeEstimator>,
248251
network_graph: Arc<NetworkGraph>,
249252
keys_manager: Arc<KeysManager>,
250253
payment_store: Arc<PaymentStore<K, L>>,
@@ -260,15 +263,16 @@ where
260263
pub fn new(
261264
event_queue: Arc<EventQueue<K, L>>, wallet: Arc<Wallet>,
262265
channel_manager: Arc<ChannelManager<K>>, tx_broadcaster: Arc<Broadcaster>,
263-
network_graph: Arc<NetworkGraph>, keys_manager: Arc<KeysManager>,
264-
payment_store: Arc<PaymentStore<K, L>>,
266+
fee_estimator: Arc<FeeEstimator>, network_graph: Arc<NetworkGraph>,
267+
keys_manager: Arc<KeysManager>, payment_store: Arc<PaymentStore<K, L>>,
265268
runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>, logger: L, config: Arc<Config>,
266269
) -> Self {
267270
Self {
268271
event_queue,
269272
wallet,
270273
channel_manager,
271274
tx_broadcaster,
275+
fee_estimator,
272276
network_graph,
273277
keys_manager,
274278
payment_store,
@@ -584,7 +588,7 @@ where
584588

585589
let output_descriptors = &outputs.iter().collect::<Vec<_>>();
586590
let tx_feerate = self
587-
.wallet
591+
.fee_estimator
588592
.get_est_sat_per_1000_weight(ConfirmationTarget::NonAnchorChannelFee);
589593

590594
// We set nLockTime to the current height to discourage fee sniping.

src/fee_estimator.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use crate::logger::{log_error, log_trace, Logger};
2+
use crate::Error;
3+
4+
use lightning::chain::chaininterface::{
5+
ConfirmationTarget, FeeEstimator, FEERATE_FLOOR_SATS_PER_KW,
6+
};
7+
8+
use bdk::FeeRate;
9+
use esplora_client::AsyncClient as EsploraClient;
10+
11+
use std::collections::HashMap;
12+
use std::ops::Deref;
13+
use std::sync::RwLock;
14+
15+
pub(crate) struct OnchainFeeEstimator<L: Deref>
16+
where
17+
L::Target: Logger,
18+
{
19+
fee_rate_cache: RwLock<HashMap<ConfirmationTarget, FeeRate>>,
20+
esplora_client: EsploraClient,
21+
logger: L,
22+
}
23+
24+
impl<L: Deref> OnchainFeeEstimator<L>
25+
where
26+
L::Target: Logger,
27+
{
28+
pub(crate) fn new(esplora_client: EsploraClient, logger: L) -> Self {
29+
let fee_rate_cache = RwLock::new(HashMap::new());
30+
Self { fee_rate_cache, esplora_client, logger }
31+
}
32+
33+
pub(crate) async fn update_fee_estimates(&self) -> Result<(), Error> {
34+
let confirmation_targets = vec![
35+
ConfirmationTarget::OnChainSweep,
36+
ConfirmationTarget::MaxAllowedNonAnchorChannelRemoteFee,
37+
ConfirmationTarget::MinAllowedAnchorChannelRemoteFee,
38+
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee,
39+
ConfirmationTarget::AnchorChannelFee,
40+
ConfirmationTarget::NonAnchorChannelFee,
41+
ConfirmationTarget::ChannelCloseMinimum,
42+
];
43+
for target in confirmation_targets {
44+
let num_blocks = match target {
45+
ConfirmationTarget::OnChainSweep => 6,
46+
ConfirmationTarget::MaxAllowedNonAnchorChannelRemoteFee => 1,
47+
ConfirmationTarget::MinAllowedAnchorChannelRemoteFee => 1008,
48+
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => 144,
49+
ConfirmationTarget::AnchorChannelFee => 1008,
50+
ConfirmationTarget::NonAnchorChannelFee => 12,
51+
ConfirmationTarget::ChannelCloseMinimum => 144,
52+
};
53+
54+
let estimates = self.esplora_client.get_fee_estimates().await.map_err(|e| {
55+
log_error!(
56+
self.logger,
57+
"Failed to retrieve fee rate estimates for {:?}: {}",
58+
target,
59+
e
60+
);
61+
Error::FeerateEstimationUpdateFailed
62+
})?;
63+
64+
let converted_estimates = esplora_client::convert_fee_rate(num_blocks, estimates)
65+
.map_err(|e| {
66+
log_error!(
67+
self.logger,
68+
"Failed to convert fee rate estimates for {:?}: {}",
69+
target,
70+
e
71+
);
72+
Error::FeerateEstimationUpdateFailed
73+
})?;
74+
75+
let fee_rate = FeeRate::from_sat_per_vb(converted_estimates);
76+
77+
// LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that
78+
// require some post-estimation adjustments to the fee rates, which we do here.
79+
let adjusted_fee_rate = match target {
80+
ConfirmationTarget::MaxAllowedNonAnchorChannelRemoteFee => {
81+
let really_high_prio = fee_rate.as_sat_per_vb() * 10.0;
82+
FeeRate::from_sat_per_vb(really_high_prio)
83+
}
84+
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => {
85+
let slightly_less_than_background = fee_rate.fee_wu(1000) - 250;
86+
FeeRate::from_sat_per_kwu(slightly_less_than_background as f32)
87+
}
88+
_ => fee_rate,
89+
};
90+
91+
let mut locked_fee_rate_cache = self.fee_rate_cache.write().unwrap();
92+
locked_fee_rate_cache.insert(target, adjusted_fee_rate);
93+
log_trace!(
94+
self.logger,
95+
"Fee rate estimation updated for {:?}: {} sats/kwu",
96+
target,
97+
adjusted_fee_rate.fee_wu(1000)
98+
);
99+
}
100+
Ok(())
101+
}
102+
103+
pub(crate) fn estimate_fee_rate(&self, confirmation_target: ConfirmationTarget) -> FeeRate {
104+
let locked_fee_rate_cache = self.fee_rate_cache.read().unwrap();
105+
106+
let fallback_sats_kwu = match confirmation_target {
107+
ConfirmationTarget::OnChainSweep => 5000,
108+
ConfirmationTarget::MaxAllowedNonAnchorChannelRemoteFee => 25 * 250,
109+
ConfirmationTarget::MinAllowedAnchorChannelRemoteFee => FEERATE_FLOOR_SATS_PER_KW,
110+
ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee => FEERATE_FLOOR_SATS_PER_KW,
111+
ConfirmationTarget::AnchorChannelFee => 500,
112+
ConfirmationTarget::NonAnchorChannelFee => 1000,
113+
ConfirmationTarget::ChannelCloseMinimum => 500,
114+
};
115+
116+
// We'll fall back on this, if we really don't have any other information.
117+
let fallback_rate = FeeRate::from_sat_per_kwu(fallback_sats_kwu as f32);
118+
119+
*locked_fee_rate_cache.get(&confirmation_target).unwrap_or(&fallback_rate)
120+
}
121+
}
122+
123+
impl<L: Deref> FeeEstimator for OnchainFeeEstimator<L>
124+
where
125+
L::Target: Logger,
126+
{
127+
fn get_est_sat_per_1000_weight(&self, confirmation_target: ConfirmationTarget) -> u32 {
128+
(self.estimate_fee_rate(confirmation_target).fee_wu(1000) as u32)
129+
.max(FEERATE_FLOOR_SATS_PER_KW)
130+
}
131+
}

0 commit comments

Comments
 (0)